Что такое SPA

Зачем нужно

Single Page Application (SPA) — это веб-приложение, которое загружается один раз и динамически обновляет содержимое страницы без полной перезагрузки. Вместо классической модели «запрос → сервер → новая HTML-страница» SPA получает данные через API и перерисовывает только нужные части интерфейса.

Понимание SPA — фундамент для работы с React, Vue, Angular и любым современным фронтенд-фреймворком.

Где используется

  • Веб-приложения с богатым интерфейсом (Gmail, Trello, Notion)
  • Дашборды и админ-панели
  • Чаты, мессенджеры, real-time приложения
  • Интерактивные редакторы и инструменты

SPA vs MPA

Multi-Page Application (MPA)

Пользователь кликает ссылку
        ↓
Браузер отправляет запрос на сервер
        ↓
Сервер генерирует полную HTML-страницу
        ↓
Браузер загружает и рендерит новую страницу
        ↓
Белый экран на момент загрузки

Single Page Application (SPA)

Пользователь кликает ссылку
        ↓
JavaScript перехватывает клик (preventDefault)
        ↓
JS запрашивает только данные (JSON) через API
        ↓
JS обновляет DOM — перерисовывает нужную часть
        ↓
URL меняется через History API (без перезагрузки)

Сравнительная таблица

Критерий SPA MPA
Первая загрузка Медленнее (загружает JS-бандл) Быстрее (только HTML)
Навигация Мгновенная Перезагрузка страницы
SEO Сложнее (нужен SSR/SSG) Нативная поддержка
Сервер Отдаёт статику + API Рендерит HTML
UX Плавный, app-like Классический
Сложность Выше Ниже

Client-Side Rendering (CSR)

SPA использует клиентский рендеринг — браузер получает минимальный HTML и JavaScript, который строит интерфейс:

<!-- Начальный HTML от сервера -->
<!DOCTYPE html>
<html>
<head>
  <title>My SPA</title>
</head>
<body>
  <!-- Пустой контейнер — JS заполнит его -->
  <div id="app"></div>

  <!-- Весь интерфейс строится этим скриптом -->
  <script src="/bundle.js"></script>
</body>
</html>
// bundle.js — клиентский код строит UI
const app = document.getElementById('app');

function render(page) {
  switch (page) {
    case 'home':
      app.innerHTML = `
        <h1>Главная</h1>
        <p>Добро пожаловать!</p>
        <a href="/about" data-link>О нас</a>
      `;
      break;
    case 'about':
      app.innerHTML = `
        <h1>О нас</h1>
        <p>Мы делаем SPA!</p>
        <a href="/" data-link>На главную</a>
      `;
      break;
  }
}

render('home');

History API

History API позволяет менять URL в адресной строке без перезагрузки страницы:

// === pushState — добавить запись в историю ===
// pushState(state, title, url)
history.pushState({ page: 'about' }, '', '/about');

// === replaceState — заменить текущую запись ===
history.replaceState({ page: 'home' }, '', '/');

// === popstate — событие при нажатии Назад/Вперёд ===
window.addEventListener('popstate', (event) => {
  console.log('Навигация:', event.state);
  // event.state — объект, переданный в pushState
  if (event.state) {
    render(event.state.page);
  }
});

Минимальный SPA-роутер на History API

// Простейший SPA-роутер
class Router {
  constructor {
    this.routes = {};

    // Обработка кнопок Назад/Вперёд
    window.addEventListener('popstate', () => {
      this.handleRoute(window.location.pathname);
    });

    // Перехват кликов по ссылкам
    document.addEventListener('click', (e) => {
      const link = e.target.closest('[data-link]');
      if (link) {
        e.preventDefault();
        const path = link.getAttribute('href');
        this.navigate(path);
      }
    });
  }

  // Регистрация маршрута
  addRoute(path, handler) {
    this.routes[path] = handler;
  }

  // Навигация
  navigate(path) {
    history.pushState({ path }, '', path);
    this.handleRoute(path);
  }

  // Обработка маршрута
  handleRoute(path) {
    const handler = this.routes[path];
    if (handler) {
      handler;
    } else {
      this.routes['/404']?.;
    }
  }

  // Запуск
  start {
    this.handleRoute(window.location.pathname);
  }
}

// Использование
const router = new Router();
const app = document.getElementById('app');

router.addRoute('/', () => {
  app.innerHTML = '<h1>Главная</h1><a href="/about" data-link>О нас</a>';
});

router.addRoute('/about', () => {
  app.innerHTML = '<h1>О нас</h1><a href="/" data-link>На главную</a>';
});

router.addRoute('/404', () => {
  app.innerHTML = '<h1>404 — Страница не найдена</h1>';
});

router.start();

Преимущества SPA

  1. Плавный UX — нет мерцания при переходах, приложение ощущается как нативное
  2. Быстрая навигация — после первой загрузки переходы мгновенны
  3. Разделение ответственности — фронтенд и бэкенд разрабатываются независимо
  4. Переиспользование API — один бэкенд для веба, мобильного приложения, десктопа
  5. Оффлайн-возможности — с Service Workers SPA работает без интернета
  6. Кэширование — статика кэшируется агрессивно, данные запрашиваются через API

Недостатки SPA

  1. SEO — поисковые роботы видят пустой HTML (решение: SSR, SSG, prerendering)
  2. Первая загрузка — большой JS-бандл загружается долго (решение: code splitting, lazy loading)
  3. Сложность — роутинг, state management, сборка — всё ложится на фронтенд
  4. Память — утечки памяти при долгих сессиях без перезагрузки
  5. JavaScript зависимость — без JS приложение не работает совсем
  6. Безопасность — XSS-атаки опаснее, токены хранятся на клиенте

Современные подходы к рендерингу

CSR (Client-Side Rendering)
  → Рендеринг полностью в браузере
  → SPA в чистом виде

SSR (Server-Side Rendering)
  → Сервер рендерит HTML для первого запроса
  → Далее работает как SPA (hydration)
  → Next.js, Nuxt.js

SSG (Static Site Generation)
  → HTML генерируется на этапе сборки
  → Максимальная скорость
  → Astro, Gatsby, Next.js (static export)

ISR (Incremental Static Regeneration)
  → Статика обновляется по запросу
  → Баланс между SSG и SSR
  → Next.js

Частые ошибки

  1. Нет fallback на сервере — при прямом заходе на /about сервер возвращает 404, нужно настроить redirect на index.html
  2. Не обрабатывают popstate — кнопка «Назад» не работает, забыли подписаться на событие
  3. Огромный бандл — не делают code splitting, загружают всё приложение сразу
  4. Игнорируют SEO — для публичных страниц SPA без SSR/SSG невидим для поисковиков
  5. Утечки памяти — не очищают слушатели событий, таймеры, подписки при уходе со страницы

Практика

  1. Собрать минимальный SPA-роутер на History API (без фреймворков)
  2. Добавить переходы между 3 страницами без перезагрузки
  3. Реализовать обработку кнопки «Назад»
  4. Настроить dev-сервер с fallback на index.html
  5. Измерить размер бандла и применить code splitting

Связанные темы

Ресурсы