Angular
SSR и гидрация
Чистое клиентское приложение отдаёт пустой div и заставляет браузер скачать, распарсить и выполнить весь JavaScript, прежде чем покажет хоть что-то. Поисковые роботы и медленные устройства страдают. SSR рендерит готовый HTML на сервере: пользователь видит контент сразу. Гидрация - это аккуратная стыковка: клиентский Angular переиспользует уже существующий серверный DOM вместо того, чтобы стереть его и отрисовать заново.
- E-commerce и медиа: SSR ради SEO и быстрого LCP на товарных и контентных страницах
- Лендинги: мгновенный показ контента до загрузки фреймворка
- Медленные сети и устройства: видимый контент пока скачивается JS
- Соцсети: корректные og-теги и превью при шаринге ссылок
- Гидрация без мерцания: серверный DOM переиспользуется, экран не перерисовывается с нуля
Предварительные знания
- Роутинг Angular: как маршруты сопоставляются компонентам
- Различие между рендером на сервере и в браузере
- Базовое понимание жизненного цикла компонента и DOM
От стирания DOM к его переиспользованию
Ранний серверный рендеринг в Angular (Angular Universal) умел отдавать HTML, но при загрузке клиента стирал серверный DOM и отрисовывал всё заново. Это вызывало мерцание и теряло смысл серверного рендера. Angular 16 (2023) принёс полную гидрацию без разрушения: клиент переиспользует серверный DOM. Angular 17 интегрировал SSR в стандартный ng new. Angular 18 (2024) добавил воспроизведение событий, а Angular 19-21 - инкрементальную гидрацию. Гидрация стала включаться по умолчанию в SSR-приложениях.
Настройка SSR и гидрации
Создать SSR-приложение можно командой ng new --ssr или добавить SSR в существующее через ng add @angular/ssr. Angular настроит серверный entry-point и сборку. Гидрация на клиенте включается одним провайдером в конфигурации приложения.
Без provideClientHydration клиент при загрузке стёр бы серверный DOM и отрисовал всё заново, вызвав мерцание. С этим провайдером Angular обходит серверный DOM, сопоставляет его со структурой компонентов и присоединяет к нему обработчики, не пересоздавая элементы.
Гидрация в Angular - non-destructive: серверный HTML остаётся на месте, а клиент лишь оживляет его. Это убирает мерцание (FOUC) и сохраняет уже отрисованный браузером результат, включая позицию прокрутки и состояние полей ввода.
Что произойдёт с серверным DOM, если SSR настроен, но provideClientHydration не добавлен?
Воспроизведение событий
Между показом серверного HTML и завершением гидрации есть окно, когда контент виден, но обработчики ещё не присоединены. Клик в это окно при обычной гидрации потерялся бы. Воспроизведение событий (event replay) решает это: ранние события записываются и проигрываются после гидрации.
Небольшой скрипт на странице слушает события на серверном HTML до гидрации и складывает их в очередь. Когда Angular присоединил обработчики, очередь воспроизводится: клик, сделанный до готовности приложения, отрабатывает так, будто приложение уже было живым.
Воспроизведение событий особенно важно для INP в Core Web Vitals: оно убирает класс потерянных взаимодействий в первые секунды после загрузки, когда пользователь видит готовую страницу, но приложение ещё гидрируется.
Какую проблему решает withEventReplay()?
Рассинхрон гидрации и как его избежать
Гидрация требует, чтобы DOM, отрисованный на сервере, совпадал с тем, что ожидает клиент. Если структуры расходятся, возникает hydration mismatch: Angular не может сопоставить узлы и в этом поддереве откатывается к разрушительной перерисовке, теряя выгоду и иногда показывая ошибку в консоли.
| Источник рассинхрона | Почему ломает | Решение |
|---|---|---|
| Прямые манипуляции DOM через innerHTML | Клиент не знает об изменениях вне Angular | Менять DOM только через шаблон и привязки |
| Обращение к window или document на сервере | На сервере их нет, рендер расходится | Проверять платформу через isPlatformBrowser или afterNextRender |
| Недетерминированный рендер (Math.random, Date.now) | Сервер и клиент дают разный результат | Фиксировать значение или вычислять после гидрации |
| Невалидный HTML (например, div внутри p) | Браузер чинит разметку, структура расходится | Соблюдать корректную вложенность тегов |
afterNextRender выполняется только в браузере и только после гидрации, поэтому внутри него безопасно обращаться к window и измерять DOM. Чтение window прямо в конструкторе или в ngOnInit упадёт на сервере или вызовет рассинхрон.
Почему чтение window.innerWidth прямо в конструкторе компонента ломает гидрацию?
Связь с другими темами
SSR опирается на роутер и ведёт к инкрементальной гидрации:
- Роутинг — Сервер рендерит компонент маршрута по URL запроса
- Инкрементальная гидрация — Следующий шаг: гидрировать не всё дерево сразу, а части по мере надобности
Итог
- SSR рендерит HTML на сервере, чтобы пользователь и поисковые роботы видели контент до загрузки JS
- Гидрация переиспользует серверный DOM на клиенте без его стирания и повторной отрисовки
- provideClientHydration() включает гидрацию, withEventReplay() добавляет воспроизведение ранних событий
- Рассинхрон сервера и клиента ломает гидрацию: DOM должен быть одинаковым на обеих сторонах
- Источники рассинхрона: прямые манипуляции DOM, обращение к window на сервере, недетерминированный рендер
Связанные уроки
- ng-22-router-intro — SSR рендерит маршруты на сервере, поэтому работу роутера нужно понимать заранее
- ng-35-incremental-hydration — Инкрементальная гидрация - развитие полной гидрации, разбираемой здесь