延迟水合需按DOM区域、网络状态、交互意图分阶段激活,而非统一延至onload;应在DOMContentLoaded后结合IntersectionObserver与requestIdleCallback,对可视且满足条件的区域启动hydration,并通过data属性标记、AbortController中断及状态同步实现可控降级。

延迟水合(deferred hydration)不是“把 hydrate 拖到 onload 之后”,而是按 DOM 区域、网络状态、交互意图分阶段激活——错配时机或粗粒度控制,反而会让首屏卡顿更明显。
hydration 为什么不能统一延迟到 window.onload
浏览器在 window.onload 触发时,图片、字体、第三方脚本等资源才全部就绪,但此时用户早已开始滚动、点击;若此时才批量执行 hydrate,会集中触发大量事件监听绑定、组件 mount 和 layout 计算,造成主线程长时间阻塞。更糟的是,onload 不保证 DOM 已稳定——某些动态插入的 <iframe> 或 Web Component 可能还在解析中。
- 真实瓶颈常出现在 hydration 阶段的 layout thrashing:比如 20 个
useState同步更新导致 5 次重排 -
DOMContentLoaded是更安全的起点,但必须配合 DOM 结构裁剪——否则 hydrate 仍会遍历并接管所有非首屏节点 - 推荐以
requestIdleCallback+IntersectionObserver组合驱动:仅对可视区 + 即将交互的区域启动 hydrate
如何用 IntersectionObserver 控制 hydration 区域
关键不是“监听是否进入视口”,而是“监听是否具备 hydrate 条件”:比如网络状态达标、JS bundle 已加载、且该区域 DOM 已完成 SSR 渲染(可通过 data-hydratable 标记识别)。
- 给待 hydrate 的容器加属性:
<section data-hydratable="true" data-hydration="comments"> - observer 回调中检查
navigator.connection.effectiveType,弱网('2g'或'3g')下跳过 hydrate,只保留骨架占位 - 避免对每个元素单独 observe:用委托方式监听父容器,再用
entry.target.querySelector('[data-hydratable]')批量提取 - hydrate 完成后立即移除 observer 引用,防止内存泄漏
SSR 输出 HTML 时必须做的三处标记
没有这些标记,客户端 hydration 就是盲操作——你无法判断哪部分该延迟、哪部分该禁用、哪部分该降级。
立即学习“前端免费学习笔记(深入)”;
- 首屏核心区域加
data-hydrate="eager":如<main data-hydrate="eager">,确保 LCP 元素立即 hydrate - 非首屏区域统一用
data-hydrate="lazy"+data-hydration-id,便于后续按需 fetch 对应 JS chunk - 所有可能被延迟 hydrate 的节点,必须预置
data-hydrated="false"属性,hydration 函数执行后改为true,避免重复执行
AbortController 在 hydration 资源加载中的实际用法
hydration 不只是运行 JS,还常伴随动态 import() 加载组件逻辑。若用户快速滚动或网络中断,这些加载必须可取消,否则会堆积 pending promise 并占用内存。
- 每次
import()前创建新AbortController,信号传入import(..., { signal }) - 监听
scroll或visibilitychange,触发controller.abort()——注意不要 abort 已 resolve 的 promise - 错误处理不能只依赖
catch:需检查error.name === 'AbortError',区别于网络失败或语法错误 - 超时设为 1200ms 而非默认的 0,避免弱网下无限等待;超时后 fallback 到轻量静态渲染
真正难的不是写一个延迟 hydrate 函数,而是让每个 hydration 单元都携带可中断、可降级、可感知的状态信号——DOM 层面的 data- 属性、网络层的 AbortSignal、应用层的 hydration 状态机,三者必须对齐。漏掉任意一环,延迟就变成卡顿。



















