Qwik 的 SSR 和 CSR 是如何协同工作的?
传统 SSR 框架在服务端渲染 HTML 后,客户端还需要重新下载和执行 JavaScript 来"水合"页面,恢复事件绑定和状态。这个过程随着应用规模增长,开销越来越大。Qwik 提出了完全不同的方案——恢复性(Resumability),让 SSR 产出的 HTML 自带全部状态和事件信息,客户端无需水合即可直接交互。
Qwik 的 SSR 渲染流程
Qwik 在服务器端执行组件渲染时,不仅生成 HTML 结构,还会将组件状态、事件处理器引用、组件层级关系等全部序列化到 HTML 中。最终返回给浏览器的 HTML 包含了完整的应用快照。
具体流程如下:
- 执行组件渲染:Qwik 在 Node.js 环境中执行组件函数,生成虚拟 DOM 并渲染为 HTML 字符串
- 收集状态和事件:渲染过程中,Qwik 收集所有
useSignal、useStore等响应式状态的当前值,以及所有onClick$等$后缀事件处理器的引用 - 序列化到 HTML:将状态数据以 JSON 格式注入到 HTML 末尾的
<script type="qwik/json">标签中,事件绑定信息以 HTML 属性形式嵌入对应 DOM 节点 - 发送完整 HTML:服务器将包含状态和事件元数据的 HTML 响应发送给浏览器
tsxexport const App = component$(() => { const count = useSignal(0); return ( <div> <p>Count: {count.value}</p> <button onClick$={() => count.value++}> Increment </button> </div> ); });
服务端渲染后,count.value 的值 0 会被序列化到 HTML 中,onClick$ 处理器会被 Qwik Optimizer 编译为一个独立的 chunk 文件,HTML 中只保留该 chunk 的引用路径。浏览器首次加载时不需要下载这段逻辑代码。
这样做的直接好处是:首屏 HTML 包含完整可交互内容,搜索引擎可以直接抓取;客户端零 JavaScript 启动成本,FCP 和 TTI 几乎同时达成。
客户端恢复:零水合的交互激活
浏览器收到 HTML 后,Qwik 的客户端工作方式与传统框架完全不同。传统框架需要下载整个应用的 JavaScript,重新执行组件渲染函数,逐个绑定事件监听器——这就是水合过程。Qwik 跳过了这整个步骤。
qwikloader 全局事件代理
Qwik 在 HTML 中注入了一个约 1KB 的 qwikloader 脚本,它做一件事:在 document 上监听所有 DOM 事件。当用户点击按钮时,事件冒泡到 document,qwikloader 根据事件目标元素上的 HTML 属性找到对应的事件处理器 chunk 路径,然后动态加载并执行该 chunk。
html<!-- 服务端渲染后的 HTML 片段 --> <button on:click="./app_component_ClickHandler.js#default"> Increment </button> <script type="qwik/json"> {"state":{"count":"0"},"refs":{}} </script>
这意味着:页面加载时不下载任何组件代码和事件处理器代码,只在用户真正交互时按需加载对应的代码块。
状态反序列化
当事件处理器 chunk 加载后,Qwik 从 <script type="qwik/json"> 中反序列化状态,将 count 恢复为响应式的 useSignal 对象。组件函数不需要重新执行,状态直接恢复到服务端渲染时的快照。
细粒度代码分割
Qwik Optimizer 编译器在构建阶段自动进行细粒度代码分割:
- 组件级分割:每个
component$()包裹的组件生成独立 chunk - 事件处理器级分割:每个
onClick$、onChange$等$后缀回调生成独立 chunk - 状态更新逻辑分割:涉及状态变更的逻辑单独提取
$ 后缀是 Qwik 的核心语法约定,它告诉编译器"这个函数的闭包需要被提取为独立模块"。这就是为什么 Qwik 要求事件处理器使用 onClick$ 而非 onClick——编译器需要显式知道哪些函数边界可以被分割。
Qwik City 的 SSR 能力
Qwik City 是 Qwik 的全栈框架,提供了路由、数据获取和服务端操作能力。
路由数据加载
routeLoader$ 在服务端执行数据获取,结果随 HTML 一起序列化发送:
tsximport { component$ } from '@builder.io/qwik'; import { routeLoader$ } from '@builder.io/qwik-city'; export const useProductData = routeLoader$(async ({ params, env }) => { const response = await fetch(`https://api.example.com/products/${params.id}`); return response.json(); }); export default component$(() => { const product = useProductData(); return ( <div> <h1>{product.value.name}</h1> <p>{product.value.description}</p> </div> ); });
routeLoader$ 的 $ 后缀同样是分割标记——数据获取逻辑在服务端执行,结果序列化后客户端直接使用,不需要重复请求。
服务端操作
action$ 处理表单提交等写操作,同样在服务端执行:
tsximport { action$ } from '@builder.io/qwik-city'; export const useAddToCart = action$(async (data, { requestEvent }) => { const session = requestEvent.sharedMap.get('session'); // 服务端执行业务逻辑 return { success: true }; });
流式 SSR
Qwik City 支持流式 SSR,服务器可以在渲染完成前就开始向客户端发送 HTML 片段。对于数据量较大的页面,用户可以更快看到首屏内容,而不是等待整个页面渲染完毕才收到第一个字节。
与传统 SSR 框架的关键区别
| 维度 | Qwik | Next.js (React) | Nuxt (Vue) |
|---|---|---|---|
| 客户端激活方式 | 恢复性,无需水合 | 水合,重新执行组件 | 水合,重新执行组件 |
| 首屏 JS 体积 | 接近零(仅 qwikloader ~1KB) | 较大(框架运行时 + 组件代码) | 中等(框架运行时 + 组件代码) |
| 代码分割粒度 | 编译器自动按函数级分割 | 需手动配置 dynamic import | 需手动配置或依赖约定路由 |
| TTI 表现 | FCP 与 TTI 几乎一致 | TTI 明显滞后于 FCP | TTI 滞后于 FCP |
| 状态传递 | 自动序列化到 HTML | 需手动处理服务端状态注入 | 需手动处理或依赖框架约定 |
核心差异在于水合成本。Next.js 的一个典型 SSR 页面,即便 HTML 已经包含完整内容,客户端仍需下载 React 运行时(约 40KB gzipped)和所有页面组件代码来执行水合。Qwik 完全跳过这一步,首次加载仅需 qwikloader 的 ~1KB。
实际性能表现
Qwik 官方基准测试中,一个中等复杂度页面的性能指标:
- FCP(首次内容绘制):约 0.3s
- TTI(可交互时间):约 0.9s
- 首次加载 JS 体积:约 1KB(qwikloader)
作为对比,相同页面在 Next.js 下的典型数据:
- FCP:约 0.5s
- TTI:约 2.5s(需要等待水合完成)
- 首次加载 JS 体积:约 80-150KB(React 运行时 + 组件代码)
大众点评 M 站在 2026 年初完成基于 Qwik.js 的重构,生产环境验证了恢复性方案在大型电商场景下的性能收益——首屏加载速度提升约 40%,TTI 从 3.2s 降至 1.1s。
最佳实践
服务端数据获取优先
优先使用 routeLoader$ 在服务器获取数据,避免客户端额外请求。数据随 HTML 一起序列化,客户端直接使用。
合理使用客户端任务
仅在必须访问浏览器 API 时使用 useVisibleTask$,如需要 window、document 或 Web API 的场景。不要用它来获取可以 SSR 的数据。
利用 useResource$ 处理异步数据
useResource$ 适合需要客户端动态获取数据的场景,它返回的 Resource 对象可以追踪加载状态(pending / resolved / rejected),便于在 UI 中展示 loading 态:
tsxexport default component$(() => { const searchData = useResource$(async ({ cleanup }) => { const controller = new AbortController(); cleanup(() => controller.abort()); const res = await fetch(`/api/search?q=keyword`, { signal: controller.signal }); return res.json(); }); return ( <Resource value={searchData} onPending={() => <p>加载中...</p>} onResolved={(data) => <div>{data.result}</div>} /> ); });
混合渲染策略
静态内容(如博客文章、产品描述)使用 SSR 确保搜索引擎可抓取;动态交互(如购物车、搜索建议)依赖 Qwik 的按需加载机制,只在用户交互时加载对应逻辑。
Qwik 的恢复性架构让 SSR 和 CSR 不再是二选一的取舍,而是一个连续光谱上的不同策略。服务端负责渲染和数据获取,客户端负责交互和按需加载,两者通过序列化机制无缝衔接,开发者不需要手动处理状态同步和水合逻辑。