
原文:Incremental Hydration In Angular Apps 翻译:TUARAN 欢迎关注 {{前端周刊}},每周更新国外论坛的前端热门文章,紧跟时事,掌握前端技术动态。

Angular 的增量水合(Incremental Hydration)通过把“可见”与“可交互”的成本拆开:页面仍然用 SSR 很快渲染出完整 HTML(有利于 LCP/SEO),但把某些区域的客户端激活(事件绑定、变更检测等)推迟到「空闲 / 进入视口 / 交互 / 定时」等触发时机,从而减少主线程阻塞(TBT)并让首屏更“顺滑”。
@defer 的加载与水合双触发现代 Web 应用经常陷入一个悖论:
问题在于:“看起来 ready”与“用起来 ready”之间存在时间差。
在传统水合(hydration)里,浏览器需要在主线程上启动框架、遍历 DOM、挂载监听器等。用户看到页面已经“完整”,但点击按钮没有反应、菜单卡住——这段时间就像性能的“恐怖谷”,通常发生在 LCP(内容已绘制)到 TTI(Time to Interactive)之间。
Angular 的增量水合把应用视为一组相对独立的“岛屿”:不是启动时把整棵组件树一次性激活,而是让某些部分在合适的时机再醒来。收益通常体现在更低的 TBT(Total Blocking Time)与更快的“体感可用”。

理解 Angular 的水合语法之前,先把“hydration”在不同阶段的含义理清:
在一些旧方案里,“水合”更像是误用:
这会导致闪烁(flicker)与大量计算开销。
Angular 16 引入非破坏式水合:
这是巨大进步,但仍是“一刀切”:启动时仍要把整棵树都水合。
增量水合建立在非破坏式水合之上,但进一步提供“粒度”。它基于 @defer 块作为边界:可以让某些组件子树先以静态 HTML(dehydrated)呈现,等触发条件满足时再执行客户端逻辑并挂载监听。
它与“懒加载(lazy loading)”的关键差异是:
因此它更像是在“保持视觉完整”的前提下,优化主线程执行成本。
@defer 的加载与水合双触发在增量水合语境里,一个 @defer 块实际控制两件事:
这意味着你可以做出更“精细”的性能画像:先把代码悄悄拉下来,但把激活推迟到真正需要的时候。
@defer (on idle; hydrate on interaction) {
<app-heavy-chart />
}<app-heavy-chart />,用户立刻看到内容。on idle:浏览器空闲时在后台拉取图表的 JS。hydrate on interaction:先不执行图表逻辑,让它保持“静态壳”;主线程保持轻。on idle 会影响该块什么时候真正渲染。下面这些是更偏“水合时机”的触发类型(不同版本/文档里表述略有差异,核心思想一致):
hydrate on idle(默认型优化)
requestIdleCallback 的时机)。hydrate on viewport(首选的“屏外内容”策略)
IntersectionObserver)。hydrate on interaction(重组件“按需启动”)
hydrate on hover(提前一点点)
hydrate on timer (X)(按时间排队)
hydrate on immediate(关键交互)
@defer (hydrate on immediate) {
<hero-cta-button />
} @placeholder {
<div>Loading...</div>
}hydrate when <condition>(条件门控)
@defer 上可靠评估;父块还没水合时,子块条件也无法被计算。hydrate never(纯静态块)
开启增量水合通常只是一处配置,但真正的关键点在于:交互触发的那一下不能丢。
在 app.config.ts 中启用客户端水合,并开启增量能力:
import { ApplicationConfig, provideZoneChangeDetection } from "@angular/core";
import {
provideClientHydration,
withIncrementalHydration,
} from "@angular/platform-browser";
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideClientHydration(withIncrementalHydration()),
],
};很多人第一反应是:
如果我用了
hydrate on interaction,那用户第一次点击是不是会被吞掉?
Angular 的思路是:在真正框架逻辑还没起来时,先用一段轻量脚本捕获事件,把它们缓冲起来,等对应块水合完成后再“回放”。在启用 withIncrementalHydration() 时,这类事件回放能力通常会一并启用(文档中也常提到 withEventReplay())。
事件回放大致流程:
@defer 区域内,就先暂存。hydrate on interaction。
增量水合带来收益,也带来一些你必须遵守的“架构规则”。忽略它们会导致退化(de-opt),甚至回落到破坏式重渲染。
Angular 水合是有层级的:
实践建议:把 @defer 块设计得更“自包含”,避免出现点了叶子节点却把整棵树都连锁唤醒的“瀑布效应”。
非破坏式水合依赖“复用 DOM”:服务端输出的 DOM 结构需要与浏览器最终 DOM、以及客户端期望结构严格一致。
常见坑:
<a> 嵌套 <a><p> 里塞了 <div> 这类块级元素<table> 缺 <tbody>这些都会导致水合复用失败,进而触发重建。
有人担心 @defer 会伤害 SEO。增量水合的前提是 SSR:主内容在服务端模板里已经输出成语义化 HTML,搜索引擎拿到的就是完整内容。
水合触发控制的是“什么时候执行 JS 让它可交互”,不是“内容什么时候出现”。
@placeholder 仍然需要即使 SSR 会把真实内容渲染出来,@placeholder 仍然很重要——主要用于 CSR 路由导航 的场景。
当用户通过 routerLink 在客户端导航进入某页时,该页的 @defer 更像常规延迟块:
@placeholder@defer (on viewport; hydrate on interaction) {
<comments-section />
} @placeholder {
<div class="comments-skeleton">Loading comments...</div>
}建议:给占位提供接近真实内容的尺寸,减少 CSR 下的布局抖动(CLS)。
最常见的问题是 Hydration Mismatch(水合不匹配):
服务端生成的 HTML 与客户端期望的 DOM 不一致。
本质原因是:客户端在水合时要求“可复用的 DOM”必须匹配预期;哪怕是一个文本节点差异,都会出问题。
new Date(),服务端与客户端时间不同。Math.random() 之类生成随机值。.ng-hydrating)给“醒来”的组件加高亮,观察时序。它减少了 SSR 应用里“内容已出现但还不能交互”的间隙,通过延迟/分批激活交互逻辑降低启动期主线程压力。
标准水合倾向于启动时激活整棵组件树;增量水合根据触发条件只激活需要的部分。
不等同。懒加载往往会推迟渲染;增量水合强调 SSR 先渲染出内容,再推迟交互激活。
通常不会。SSR 已输出完整内容,触发控制的是 JS 执行时机。
@defer 的作用是什么?它定义水合边界,并控制“什么时候加载代码 / 什么时候激活交互”。
增量水合的核心很简单:
你可以从这份行动清单开始:
hydrate on immediatehydrate on viewporthydrate on interactionhydrate never@placeholder:**占位尽量稳定尺寸,避免 CLS。一句话结论:不要在启动时把所有东西一次性“唤醒”。让组件在用户需要时再启动,你会得到更快、更稳、更顺滑的 Angular SSR 体验。