Что такое 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
- Плавный UX — нет мерцания при переходах, приложение ощущается как нативное
- Быстрая навигация — после первой загрузки переходы мгновенны
- Разделение ответственности — фронтенд и бэкенд разрабатываются независимо
- Переиспользование API — один бэкенд для веба, мобильного приложения, десктопа
- Оффлайн-возможности — с Service Workers SPA работает без интернета
- Кэширование — статика кэшируется агрессивно, данные запрашиваются через API
Недостатки SPA
- SEO — поисковые роботы видят пустой HTML (решение: SSR, SSG, prerendering)
- Первая загрузка — большой JS-бандл загружается долго (решение: code splitting, lazy loading)
- Сложность — роутинг, state management, сборка — всё ложится на фронтенд
- Память — утечки памяти при долгих сессиях без перезагрузки
- JavaScript зависимость — без JS приложение не работает совсем
- Безопасность — 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
Частые ошибки
- Нет fallback на сервере — при прямом заходе на
/aboutсервер возвращает 404, нужно настроить redirect наindex.html - Не обрабатывают popstate — кнопка «Назад» не работает, забыли подписаться на событие
- Огромный бандл — не делают code splitting, загружают всё приложение сразу
- Игнорируют SEO — для публичных страниц SPA без SSR/SSG невидим для поисковиков
- Утечки памяти — не очищают слушатели событий, таймеры, подписки при уходе со страницы
Практика
- Собрать минимальный SPA-роутер на History API (без фреймворков)
- Добавить переходы между 3 страницами без перезагрузки
- Реализовать обработку кнопки «Назад»
- Настроить dev-сервер с fallback на
index.html - Измерить размер бандла и применить code splitting
Связанные темы
- Роутинг в SPA — детальная реализация маршрутизации
- Управление состоянием — как хранить данные в SPA
- Virtual DOM — механизм эффективного обновления UI
- Webpack — как собирать SPA-приложения
- HTTP протокол — взаимодействие SPA с сервером