Внести вклад
Подробные руководства
Серверный и гибридный рендеринг

Гидратация

Что такое гидратация

Гидратация — это процесс восстановления на клиенте приложения, отрендеренного на сервере. Он включает повторное использование DOM-структур, созданных на сервере, сохранение состояния приложения, передачу данных, уже полученных сервером, и другие связанные шаги.

Почему гидратация важна?

Гидратация повышает производительность приложения, избегая лишней работы по повторному созданию DOM-узлов. Вместо этого Angular сопоставляет существующие DOM-элементы со структурой приложения во время выполнения и по возможности переиспользует узлы. Это даёт измеримый прирост по метрикам Core Web Vitals (CWV), например снижение First Input Delay (FID) и Largest Contentful Paint (LCP), а также Cumulative Layout Shift (CLS). Улучшение этих показателей также влияет на SEO.

Без включённой гидратации Angular-приложения с SSR уничтожают и заново рендерят DOM, что может привести к заметному мерцанию UI. Такой повторный рендеринг негативно влияет на Core Web Vitals, например LCP, и вызывает сдвиг макета. Включение гидратации позволяет переиспользовать существующий DOM и предотвращает мерцание.

Как включить гидратацию в Angular

Гидратацию можно включить только для приложений с рендерингом на стороне сервера (SSR). Сначала следуйте руководству по Angular SSR, чтобы включить серверный рендеринг.

Использование Angular CLI

Если вы включили SSR через Angular CLI (при создании приложения или позже через ng add @angular/ssr), код, включающий гидратацию, уже должен быть добавлен в приложение.

Ручная настройка

Если у вас кастомная настройка и вы не использовали Angular CLI для включения SSR, гидратацию можно включить вручную: в главном компоненте или модуле приложения импортируйте provideClientHydration из @angular/platform-browser и добавьте этот провайдер в список провайдеров при bootstrap.

import {
  bootstrapApplication,
  provideClientHydration,
} from '@angular/platform-browser';
...

bootstrapApplication(App, {
  providers: [provideClientHydration()]
});

Если вы используете NgModules, добавьте provideClientHydration в список провайдеров корневого модуля приложения.

import {provideClientHydration} from '@angular/platform-browser';
import {NgModule} from '@angular/core';

@NgModule({
  declarations: [App],
  exports: [App],
  bootstrap: [App],
  providers: [provideClientHydration()],
})
export class AppModule {}

ВАЖНО: Убедитесь, что вызов provideClientHydration() также включён в набор провайдеров, используемый для bootstrap приложения на сервере. В приложениях со стандартной структурой проекта (созданной командой ng new) добавления вызова в корневой AppModule обычно достаточно, так как этот модуль импортируется серверным модулем. При кастомной настройке добавьте provideClientHydration() в список провайдеров конфигурации bootstrap на сервере.

Проверка, что гидратация включена

После настройки гидратации и запуска сервера загрузите приложение в браузере.

ПОЛЕЗНО: Перед полной работой гидратации, скорее всего, потребуется исправить случаи прямой манипуляции DOM — перейти на конструкции Angular или использовать ngSkipHydration. Подробнее см. Ограничения, Прямая манипуляция DOM и Как пропустить гидратацию для отдельных компонентов.

В режиме разработки можно подтвердить, что гидратация включена, открыв Developer Tools в браузере и посмотрев консоль. Должно появиться сообщение со статистикой гидратации, например числом гидратированных компонентов и узлов. Angular считает статистику по всем компонентам на странице, включая сторонние библиотеки.

Также можно использовать расширение Angular DevTools, чтобы увидеть статус гидратации компонентов на странице. Angular DevTools позволяет включить оверлей, показывающий, какие части страницы были гидратированы. При ошибке несоответствия гидратации DevTools также подсветит компонент, вызвавший ошибку.

Захват и повтор событий

Когда приложение отрендерено на сервере, оно видно в браузере сразу после загрузки HTML. Пользователи могут предполагать, что со страницей уже можно взаимодействовать, но обработчики событий не привязаны, пока гидратация не завершится. Начиная с v18, можно включить функцию Event Replay, которая захватывает все события до гидратации и воспроизводит их после её завершения. Включите её с помощью функции withEventReplay(), например:

import {provideClientHydration, withEventReplay} from '@angular/platform-browser';

bootstrapApplication(App, {
  providers: [provideClientHydration(withEventReplay())],
});

Как работает повтор событий

Event Replay улучшает пользовательский опыт, захватывая события пользователя, произошедшие до завершения гидратации. Затем эти события воспроизводятся, чтобы ни одно взаимодействие не было потеряно.

Повтор событий делится на три основные фазы:

  • Захват взаимодействий пользователя
    До гидратации Event Replay захватывает и сохраняет все взаимодействия пользователя, например клики и другие нативные события браузера.

  • Хранение событий
    Event Contract держит в памяти все взаимодействия, записанные на предыдущем шаге, чтобы они не были потеряны для последующего воспроизведения.

  • Повторный запуск событий
    После завершения гидратации Angular повторно вызывает захваченные события.

Повтор событий поддерживает нативные события браузера, например click, mouseover и focusin. Подробнее о JSAction — библиотеке, на которой основан повтор событий, — можно прочитать в readme.

Эта функция обеспечивает согласованный пользовательский опыт и не даёт игнорировать действия пользователя, выполненные до гидратации.

ПРИМЕЧАНИЕ: Если включена инкрементальная гидратация, повтор событий автоматически включается «под капотом».

Ограничения

Гидратация накладывает на приложение ряд ограничений, которых нет без неё. На сервере и на клиенте должна генерироваться одна и та же структура DOM. Процесс гидратации ожидает одинаковое дерево DOM в обоих местах. Это также включает пробелы и comment-узлы, которые Angular создаёт при рендеринге на сервере. Эти пробелы и узлы должны присутствовать в HTML, сгенерированном SSR.

ВАЖНО: HTML, полученный при серверном рендеринге, не должен изменяться между сервером и клиентом.

Если структуры DOM на сервере и клиенте не совпадают, гидратация столкнётся с проблемами при сопоставлении ожидаемого и фактического DOM. Чаще всего виноваты компоненты, которые напрямую манипулируют DOM через нативные DOM API.

Прямая манипуляция DOM

Если компоненты манипулируют DOM через нативные DOM API или используют innerHTML / outerHTML, процесс гидратации столкнётся с ошибками. Типичные проблемные случаи — доступ к document, поиск конкретных элементов и добавление узлов через appendChild. Отсоединение DOM-узлов и перемещение их в другие места также приводит к ошибкам.

Angular не знает об этих изменениях DOM и не может разрешить их во время гидратации. Он ожидает определённую структуру, но встречает другую. Такое несоответствие приводит к сбою гидратации и ошибке DOM mismatch (см. ниже).

Лучше отрефакторить компонент, чтобы избежать такой манипуляции DOM. По возможности используйте Angular API. Если поведение нельзя отрефакторить сразу, используйте атрибут ngSkipHydration (описан ниже), пока не найдёте решение, совместимое с гидратацией.

Корректная HTML-структура

В ряде случаев шаблон компонента с некорректной HTML-структурой может привести к ошибке DOM mismatch при гидратации.

Ниже — самые частые примеры такой проблемы.

  • <table> без <tbody>
  • <div> внутри <p>
  • <a> внутри другого <a>

Если вы не уверены в валидности HTML, проверьте его синтаксическим валидатором.

ПРИМЕЧАНИЕ: Стандарт HTML не требует элемент <tbody> внутри таблиц, но современные браузеры автоматически создают <tbody> в таблицах без него. Из-за этого несоответствия всегда явно объявляйте <tbody> в таблицах, чтобы избежать ошибок гидратации.

Конфигурация Preserve Whitespaces

При использовании гидратации рекомендуется оставлять значение по умолчанию false для preserveWhitespaces. Если этой настройки нет в tsconfig, значение будет false, и менять ничего не нужно. Если включить сохранение пробелов через preserveWhitespaces: true в tsconfig, возможны проблемы с гидратацией. Эта конфигурация пока не полностью поддерживается.

ПОЛЕЗНО: Убедитесь, что эта настройка задана одинаково в tsconfig.server.json для сервера и в tsconfig.app.json для браузерных сборок. Разные значения сломают гидратацию.

Если вы задаёте эту настройку в tsconfig, рекомендуется указать её только в tsconfig.app.json — по умолчанию tsconfig.server.json унаследует её оттуда.

Кастомный или Noop Zone.js пока не поддерживаются

Гидратация опирается на сигнал от Zone.js о стабильности приложения, чтобы Angular мог начать сериализацию на сервере или post-hydration cleanup на клиенте — удаление DOM-узлов, которые остались «невостребованными».

Предоставление кастомной или «noop» реализации Zone.js может изменить момент события «stable» и запустить сериализацию или cleanup слишком рано или слишком поздно. Эта конфигурация пока не полностью поддерживается; возможно, потребуется скорректировать тайминг события onStable в кастомной реализации Zone.js.

Ошибки

Существует несколько ошибок, связанных с гидратацией: от несоответствия узлов до случаев, когда ngSkipHydration использован на недопустимом host-узле. Самый частый случай — прямая манипуляция DOM через нативные API, из-за которой гидратация не может найти или сопоставить ожидаемую структуру DOM на клиенте с той, что была отрендерена на сервере. Другой случай описан ранее в разделе Корректная HTML-структура. Используйте валидную HTML-структуру в шаблонах — и этой ошибки удастся избежать.

Полный справочник ошибок гидратации — в руководстве по ошибкам.

Как пропустить гидратацию для отдельных компонентов

Некоторые компоненты могут работать некорректно при включённой гидратации из-за описанных выше проблем, например прямой манипуляции DOM. Как временное решение можно добавить атрибут ngSkipHydration к тегу компонента, чтобы пропустить гидратацию всего компонента.

<app-example ngSkipHydration />

Либо задайте ngSkipHydration как host binding.

@Component({
  ...
  host: {ngSkipHydration: 'true'},
})
class ExampleComponent {}

Атрибут ngSkipHydration заставляет Angular пропустить гидратацию всего компонента и его потомков. С этим атрибутом компонент ведёт себя так, будто гидратация не включена: уничтожает себя и рендерит заново.

ПОЛЕЗНО: Это исправит проблемы рендеринга, но для этого компонента (и его потомков) вы не получите преимуществ гидратации. Чтобы убрать аннотацию пропуска гидратации, нужно изменить реализацию компонента и избежать паттернов, ломающих гидратацию (например, прямой манипуляции DOM).

Атрибут ngSkipHydration можно использовать только на host-узлах компонентов. Если добавить его к другим узлам, Angular выбросит ошибку.

Помните: добавление ngSkipHydration к корневому компоненту приложения фактически отключит гидратацию для всего приложения. Используйте этот атрибут осторожно — это крайняя мера. Компоненты, ломающие гидратацию, следует считать багами, которые нужно исправить.

Тайминг гидратации и стабильность приложения

Стабильность приложения — важная часть процесса гидратации. Гидратация и любые post-hydration процессы происходят только после того, как приложение сообщило о стабильности. Стабильность может задерживаться по разным причинам: таймауты и интервалы, неразрешённые промисы, незавершённые microtasks. В таких случаях может появиться ошибка Application remains unstable, означающая, что приложение не достигло стабильного состояния за 10 секунд. Если приложение не гидратируется сразу, посмотрите, что влияет на стабильность, и отрефакторьте код, чтобы избежать этих задержек.

Отладка стабильности приложения

Утилита provideStabilityDebugging помогает понять, почему приложение не стабилизируется. В режиме разработки она предоставляется по умолчанию при использовании provideClientHydration. Её также можно добавить вручную в провайдеры приложения для production-сборок или при SSR без гидратации. Функция пишет в консоль информацию, если приложение стабилизируется дольше ожидаемого.

import {provideStabilityDebugging} from '@angular/core';
import {bootstrapApplication} from '@angular/platform-browser';
import 'zone.js/plugins/task-tracking'; // Use if you have Zone.js with `provideZoneChangeDetection`

bootstrapApplication(App, {
  providers: [provideStabilityDebugging()],
});

При включении утилита логирует ожидающие задачи (PendingTasks) в консоль. Если приложение использует Zone.js, можно также импортировать zone.js/plugins/task-tracking, чтобы увидеть, какие macrotasks мешают Angular Zone стабилизироваться. Плагин даёт stack trace создания macrotask и по��огает найти источник задержки.

ВАЖНО: Angular не удаляет плагин отслеживания задач zone.js и эту утилиту из production-сборок. Используйте их только для временной отладки проблем стабильности в разработке, в том числе для оптимизированных production-сборок.

I18N

ПОЛЕЗНО: По умолчанию Angular пропускает гидратацию для компонентов, использующих блоки i18n, фактически заново рендеря эти компоненты с нуля.

Чтобы включить гидратацию для блоков i18n, добавьте withI18nSupport в вызов provideClientHydration.

import {
  bootstrapApplication,
  provideClientHydration,
  withI18nSupport,
} from '@angular/platform-browser';
...

bootstrapApplication(App, {
  providers: [provideClientHydration(withI18nSupport())]
});

Согласованный рендеринг на сервере и клиенте

Избегайте блоков @if и других условных конструкций, которые показывают разный контент при серверном и клиентском рендеринге — например, @if с функцией Angular isPlatformBrowser. Такие различия вызывают сдвиги макета, ухудшают UX и Core Web Vitals.

Сторонние библиотеки с манипуляцией DOM

Ряд сторонних библиотек зависит от манипуляции DOM для рендеринга. Классический пример — графики D3. Без гидратации они работали, но при включённой гидратации могут вызывать ошибки DOM mismatch. Пока при таких ошибках можно добавить атрибут ngSkipHydration к компоненту, который рендерится с помощью этой библиотеки.

Сторонние скрипты с манипуляцией DOM

Многие сторонние скрипты — трекеры рекламы, аналитика и т.п. — изменяют DOM до гидратации. Они могут вызывать ошибки гидратации, потому что страница больше не соответствует структуре, ожидаемой Angular. По возможности откладывайте такие скрипты до завершения гидратации. Рассмотрите AfterNextRender, чтобы отложить скрипт до post-hydration процессов.

Инкрементальная гидратация

Инкрементальная гидратация — продвинутая форма гидратации с более гранулярным контролем над тем, когда происходит гидратация. Подробнее см. в руководстве по инкрементальной гидратации.