Resumability(可恢复性)

Qwik 框架提出的 SSR 策略,与传统 Hydration(水合)对立。服务端的执行状态被序列化到 HTML 中,客户端直接"恢复"(resume)而非"重放"(replay),实现 TTI 接近 FCP 的近零水合加载。

核心概念

Resumability 是 Qwik 框架的核心架构理念,它从根本上重新定义了 SSR 的客户端接管方式。传统 Hydration 要求客户端重放服务端的工作——重新执行组件函数、重建状态、重新绑定事件;Resumability 则让客户端恢复服务端的执行状态——直接从 HTML 中读取序列化的状态和订阅关系,无需执行组件函数即可达到可交互状态。

核心公式:传统 SSR 的 TTI = FCP + Hydration时间;Resumability 的 TTI ≈ FCP

"接近零"而非"绝对零"qwikloader.js(约 1KB)需在页面加载时同步执行以注册全局事件代理(监听所有 DOM 事件、解析 on:click 等属性值、按需 import 对应 chunk),执行耗时通常 <1ms。此外 prefetch 策略会在浏览器空闲时预加载可能被交互的 chunk,产生额外网络请求和 JS 解析成本。但与传统水合的全量组件重执行相比,这些成本可忽略不计。

实现机制

1. 编译器做代码拆分

Qwik 的编译器(称为 Optimizer)将每个事件处理函数、每个响应式 effect 拆成独立的懒加载 chunk。拆分边界由 $ 后缀标记(如 onClick$component$):

jsx
// 源码
export const Counter = component$(() => {
  const count = useSignal(0);
  return <button onClick$={() => count.value++}>{count.value}</button>;
});

// 编译后:事件处理函数被拆到独立 chunk
// main.js(初始加载)
// → button 上只有一个字符串引用 "chunk-abc.js#handler_0"

// chunk-abc.js(用户点击时才加载)
export const handler_0 = (ctx) => {
  const count = useLexicalScope(ctx);
  count.value++;
};

2. 状态序列化到 HTML

服务端渲染时,组件状态、响应式订阅图谱、事件绑定信息全部序列化为 HTML 属性(q: 前缀)。客户端无需执行 JS 就拥有完整的应用状态描述。

3. 全局事件代理 + 按需加载

qwikloader.js 同步注册全局事件代理。用户真正交互时(如点击按钮),代理解析元素的 on:click 属性值,动态 import() 对应的 JS chunk,通过 useLexicalScope 从序列化状态恢复闭包上下文,执行事件处理逻辑。

编译器的关键角色

Resumability 的实现强依赖编译器的两项能力:

  1. 交互边界分析:编译器识别 $ 标记,按交互边界将代码拆分为最小可加载单元
  2. 闭包序列化分析:编译器分析每个 chunk 的闭包捕获变量,自动生成序列化/反序列化的胶水代码(useLexicalScope

这与 Svelte/Solid 的编译器目标完全不同——后者的编译器目标是"生成高效更新代码"(消灭 diff),Qwik 的编译器目标是"最小化初始加载的 JS 执行量"(推迟加载)。

Trade-off

优势

  • TTI 接近 FCP,大型应用首屏可交互速度远超传统水合方案
  • JS 按需加载,不交互的功能永远不下载

代价

  • 首次交互 cold start:用户第一次点击某个功能时需要下载对应 chunk,延迟约 100-300ms(可用 prefetch 缓解)
  • 序列化约束:闭包捕获的变量必须是 JSON 可序列化类型——不支持 MapSetWeakRef、DOM 引用、带原型链方法的 class 实例。整个开发模型围绕序列化约束设计
  • 心智模型$ 后缀标记是一个全新的编程范式,不是简单的"性能优化"而是架构层面的转变
  • 调试困难:代码拆分到多个 chunk 后,跨文件调试本质上比单文件困难,Source Map 精度也因此受限

与 Hydration 的对比

维度Hydration(传统水合)Resumability
客户端 JS 执行全量/部分重执行组件qwikloader 同步执行(~1KB),组件不执行
状态恢复重新计算从 HTML 反序列化
TTIFCP + 水合时间≈ FCP
首次交互延迟水合完成后无额外延迟有 chunk 下载 cold start
编译器依赖高(代码拆分 + 序列化分析)
组件边界保留高(Fiber 树 / 组件实例)低(代码拆分打散了组件边界)

引用本术语的文章