SSR и SSG — что меняется в компиляции (и что НЕ меняется)

Контекст: 00-overview. Главный вопрос — «как компилируется приложение для SSR/SSG».

Сразу главное

Компиляция компонентов/шаблонов при SSR и SSG — точно такая же, как для обычного браузерного приложения: full AOT Ivy → ɵɵdefineComponent (см. 01-full-pipeline). Те же template-функции, те же инструкции ɵɵdomElementStart/ɵɵtextInterpolate/…

SSR/SSG не трогают компилятор шаблонов. Они добавляют:

  1. второй build target — серверный бандл (кроме браузерного);
  2. другую платформу в рантайме — platform-server вместо platform-browser;
  3. шаг рендера в HTML — на запрос (SSR) или на этапе сборки (SSG/prerender);
  4. hydration — чтобы браузер не перерисовывал готовый серверный DOM.

То есть разница не в «как компилируется компонент», а в «куда и когда выполняется уже скомпилированный код».

CSR vs SSR vs SSG

CSR (обычно) SSR SSG (prerender)
Где строится HTML в браузере, в рантайме на сервере (Node), на каждый запрос на этапе сборки, заранее
Когда при загрузке у юзера при HTTP-запросе при ng build
Что отдаётся пустой index.html + JS готовый HTML + JS готовые статические .html + JS
Платформа рендера platform-browser platform-server platform-server (во время билда)
Плюс просто свежие данные + быстрый first paint + SEO мгновенная отдача (CDN), без сервера
Минус пустой экран/SEO нужен работающий Node-сервер контент «замораживается» на момент билда

Компиляция Ivy во всех трёх — одинаковая. Отличается только платформа и момент выполнения.

Что добавляет сборка: ДВА бандла

ng build с включённым SSR (@angular/build:application с опциями server/ssr/ prerender) выдаёт два выходных набора из одного и того же скомпилированного кода:

скомпилированный Ivy-код (один и тот же)
   ├─ browser bundle  → dist/.../browser/   (грузится в браузере, как обычно)
   └─ server bundle   → dist/.../server/    (запускается на Node)
        точка входа: src/main.server.ts / server.ts
        bootstrap через provideServerRendering (а не bootstrapApplication в браузере)

Серверный бандл подключает @angular/platform-server. У него нет настоящего DOM — вместо браузерного DOM используется domino (серверная реализация DOM на JS, platform-server/src/domino_adapter.ts). Поэтому те же инструкции ɵɵdomElement* создают узлы не в реальном браузере, а в domino-дереве, которое потом сериализуется в строку HTML.

SSR — поток на каждый запрос

HTTP-запрос на Node-сервер
  └─ renderApplication() / handler из @angular/ssr   (platform-server/src/utils.ts)
       ├─ createServerPlatform() → platformServer([...])   // platform-server, domino DOM
       ├─ bootstrap приложения → create+update проходы строят DOM в domino
       ├─ дождаться стабилизации (initializers, pending tasks)
       ├─ annotateForHydration() — расставить маркеры hydration (атрибуты `ngh`)
       └─ сериализовать domino-DOM → строка HTML
  → отдать HTML клиенту (быстрый first paint, готовый для SEO)
  → в браузере подхватывает browser bundle + provideClientHydration (см. ниже)

annotateForHydration (platform-server/src/utils.ts) и transfer state добавляют в HTML служебную разметку, чтобы клиент мог «переиспользовать» этот DOM, а не строить с нуля.

SSG / prerender — на этапе сборки

SSG — это тот же серверный рендер, но выполненный во время ng build, а не на запрос. Билдер:

  1. берёт серверный бандл;
  2. прогоняет его по списку маршрутов (роуты, которые умеет перечислить роутер, или заданные вручную);
  3. для каждого маршрута получает HTML-строку и пишет статический .html в dist.

Результат можно положить на CDN/статический хостинг — Node-сервер в рантайме не нужен. Минус: данные «вмёрзли» на момент сборки (для динамики — SSR или ISR-подходы).

Компиляция тут снова та же — просто скомпилированный код один раз исполняется при билде для генерации страниц.

Hydration — чтобы клиент не перерисовывал серверный DOM

Без hydration браузер выкинул бы серверный HTML и построил DOM заново (мелькание, лишняя работа). provideClientHydration() (platform-browser/src/hydration.ts) включает переиспользование DOM: Angular на клиенте обходит существующие узлы (по маркерам ngh из серверного рендера) и «оживляет» их — навешивает листенеры, связывает с LView, не пересоздавая элементы.

Связанные механизмы:

  • TransferState (core/src/transfer_state.ts) — данные, полученные на сервере (напр. HTTP-ответы), кладутся в HTML и переиспользуются на клиенте, чтобы не делать тот же запрос дважды.
  • Event replay — события, случившиеся до загрузки JS, воспроизводятся после hydration (platform-server/test/event_replay_spec.ts).
  • Incremental hydration (v17+) — @defer (hydrate on …) гидрирует куски лениво, по триггеру (platform-server/test/incremental_hydration_spec.ts).

Hydration — это рантайм-механизм, к компиляции компонентов отношения не имеет (кроме того, что @defer блоки компилируются отдельными dehydrated-видами).

Что в компиляции НЕ меняется

  • ngtsc так же генерит ɵfac/ɵcmp, та же template-функция (rf, ctx).
  • Те же стадии HtmlParser → parseTemplate → ingest → фазы → reify → emit.
  • Библиотеки так же partial + linker (02-partial-ivy-and-linker).
  • Один и тот же скомпилированный код едет и в browser-, и в server-бандл.

Меняется окружение исполнения (platform-server + domino), момент (запрос vs билд) и постобработка (сериализация в HTML + hydration-маркеры).

Привязка к исходникам

  • packages/platform-server/src/utils.tsrenderApplication/serialize + annotateForHydration
  • packages/platform-server/src/provide_server.tsprovideServerRendering
  • packages/platform-server/src/domino_adapter.ts — серверный DOM (domino)
  • packages/platform-browser/src/hydration.tsprovideClientHydration
  • packages/core/src/hydration/ — DOM reuse, incremental hydration
  • packages/core/src/transfer_state.ts — TransferState
  • @angular/ssr (отдельный пакет) — handler/Express-обвязка для SSR-сервера