一、引言
你打开一个网页,页面内容已经渲染出来了------文字、图片、布局都完美呈现。但当你点击一个按钮,它没有任何反应。过了几秒,按钮才变得可用。这关键的几秒,就是"水合税"。
这是所有 React 和 Vue SSR 应用都面临的问题。浏览器收到服务器渲染的 HTML 后,还需要下载、解析、执行整套 JavaScript 运行时和组件代码,才能让页面变得可交互。这个过程叫做水合(Hydration)。
Qwik------由 Angular 之父 Misko Hevery 创建的前端框架------提出了一种全新的解决方案:可恢复性(Resumability)。它不水合,而是从服务器中断的地方继续执行。
本文将从 Qwik 开源项目出发,介绍其核心概念,然后通过 Qwik City 元框架构建一个完整的待办事项应用。
二、水合的问题与可恢复性的答案
2.1 水合税
传统的服务器端渲染(SSR)流程如下:
-
服务器渲染 HTML → 浏览器看到页面(但不可交互)
-
浏览器下载 JavaScript 运行时 + 组件代码
-
浏览器重新执行整个组件树,附加事件处理器
-
页面变得可交互
这个流程的问题在于步骤 2 和 3 阻塞了交互性。React 的运行时约 40KB(gzipped),加上组件代码,初始 JS 包通常在 100KB 以上。用户看到页面后,必须等待这些代码下载、解析、执行完毕,才能点击任何东西。
更重要的是,页面越复杂,水合时间越长------因为所有组件都要重新执行一遍。
2.2 Qwik 的可恢复性
Qwik 的解决思路完全不同:
-
服务器渲染 HTML → 将应用状态序列化到 HTML 中
-
浏览器加载约 1.6KB 的 QwikLoader
-
QwikLoader 读取 HTML 中的序列化状态
-
用户交互时,只加载对应组件的代码
-
不需要重新执行组件树,直接恢复
关键区别在于:传统框架需要重新执行组件树来重建状态,而 Qwik 把状态保存在 HTML 中,直接恢复。
水合就像你写了一半的文档,关掉电脑,第二天打开后重新从头开始写。可恢复性就像你保存了文档,第二天打开后从昨天中断的地方继续写。显然后者更高效。
2.3 水合 vs 可恢复性对比


三、Qwik 开源项目介绍
3.1 项目背景
Qwik 由 Misko Hevery (Angular 框架创始人)创建,于 2023 年 5 月发布 1.0 稳定版。目前 GitHub 上拥有 22K+ stars,最新稳定版为 1.20.0(2026 年 5 月发布),2.0 正处于 beta 阶段。
技术栈:
-
Qwik(核心框架)+ Qwik City(元框架)
-
TypeScript + JSX
-
Vite(构建工具)
-
MIT 开源许可证
3.2 核心 API
Qwik 使用 JSX 语法,但引入了一系列 $ 后缀的 API 来标记可延迟加载的边界:
// component$ 定义可延迟加载的组件
export const Counter = component$(() => {
const count = useSignal(0); // useSignal 响应式状态
return (
<button onClick$={() => count.value++}> // onClick$ 延迟加载事件处理器
Count: {count.value}
</button>
);
});
核心 API 一览:
| API | 用途 | 说明 |
|---|---|---|
component$() |
定义组件 | 标记组件可延迟加载 |
useSignal() |
基本状态管理 | 类似 SolidJS 的信号 |
useStore() |
复杂状态对象 | 深层响应式对象 |
routeLoader$() |
服务端数据加载 | 类似 Next.js 的 getServerSideProps |
routeAction$() |
服务端操作 | 表单提交、数据修改 |
$ 后缀 |
标记懒加载边界 | 组件、事件、加载器都带 $ |
3.3 Qwik City 架构

四、实战:构建待办事项应用
4.1 项目初始化
npm create qwik@latest
# 选择 Basic App 模板
cd qwik-todo-app
npm install
npm start
4.2 目录结构
qwik-todo-app/
├── src/
│ ├── components/ # 通用组件
│ ├── routes/ # 路由目录(文件系统路由)
│ │ ├── index.tsx # 首页
│ │ ├── layout.tsx # 根布局
│ │ └── todos/ # 待办事项路由
│ │ └── index.tsx
│ ├── root.tsx # 根组件
│ ├── entry.ssr.tsx # SSR 入口
│ └── global.css # 全局样式
├── public/ # 静态资源
├── vite.config.ts # Vite 配置
└── package.json
4.3 用 routeLoader$ 加载数据
import { component$, useSignal } from '@builder.io/qwik';
import { routeLoader$, routeAction$, Form } from '@builder.io/qwik-city';
interface Todo {
id: number;
text: string;
completed: boolean;
}
// routeLoader$ 在服务端加载数据
export const useTodoList = routeLoader$(async () => {
const todos: Todo[] = [
{ id: 1, text: '学习 Qwik 基础知识', completed: true },
{ id: 2, text: '理解可恢复性概念', completed: false },
{ id: 3, text: '构建一个 Qwik 应用', completed: false },
];
return todos;
});
4.4 用 routeAction$ 处理操作
// 添加操作
export const useAddTodo = routeAction$(async (data) => {
return { success: true, message: `已添加: ${data.text}` };
});
// 删除操作
export const useDeleteTodo = routeAction$(async (data) => {
return { success: true, message: `已删除 ID: ${data.id}` };
});
4.5 构建 UI 组件
export default component$(() => {
const todos = useTodoList();
const addTodo = useAddTodo();
const deleteTodo = useDeleteTodo();
const newTodoText = useSignal('');
return (
<div style="max-width: 600px; margin: 2rem auto; padding: 1rem;">
<h1>我的待办事项</h1>
<Form action={addTodo} style="display: flex; gap: 0.5rem; margin-bottom: 1.5rem;">
<input
type="text" placeholder="输入新待办事项..."
value={newTodoText.value}
onInput$={(_, el) => { newTodoText.value = el.value }}
name="text" required
/>
<button type="submit">添加</button>
</Form>
<ul>
{todos.value.map((todo) => (
<li key={todo.id} style="display: flex; justify-content: space-between; padding: 0.75rem; border-bottom: 1px solid #eee;">
<span style={todo.completed ? 'text-decoration: line-through; color: #999;' : ''}>
{todo.text}
</span>
<Form action={deleteTodo}>
<input type="hidden" name="id" value={todo.id} />
<button type="submit" style="background: #e74c3c; color: white; border: none; border-radius: 4px; padding: 0.3rem 0.6rem; cursor: pointer;">
删除
</button>
</Form>
</li>
))}
</ul>
</div>
);
});
4.6 运行测试
npm start
# 访问 http://localhost:5173/todos
打开浏览器,你会看到待办事项列表。打开开发者工具的 Network 面板------你会发现初始加载时几乎没有 JS 代码被下载。只有当你点击"添加"或"删除"按钮时,Qwik 才会按需加载对应的处理代码。
五、Qwik vs 主流框架对比
5.1 核心架构
| 维度 | Qwik | React | Vue | Next.js |
|---|---|---|---|---|
| 核心架构 | 可恢复性 | 水合 | 水合 | 水合 + RSC |
| 初始 JS | ~1.6KB | ~40KB | ~30KB | ~40KB+ |
| 懒加载 | 自动($ 边界) | 手动(React.lazy) | 手动 | 手动 + 自动 |
| 响应式 | 信号(Signal) | 状态(useState) | 响应式(ref) | 状态(useState) |
5.2 路由与数据加载
| 维度 | Qwik City | Next.js | Nuxt |
|---|---|---|---|
| 路由方式 | 文件系统 | 文件系统 | 文件系统 |
| 数据加载 | routeLoader$ | RSC / getServerSideProps | AsyncData |
| 数据操作 | routeAction$ | Server Actions | API Routes |
| 布局系统 | layout.tsx | layout.tsx | layout.vue |
六、结语
Qwik 代表了一种全新的前端架构思路。它不试图优化水合过程,而是直接从根本上消除水合。
在我看来,Qwik 最大的贡献不是"又一个前端框架",而是证明了"可恢复性"是一个可行的架构方案。它告诉我们:浏览器不需要重新执行服务器已经做过的工作。把状态保存在 HTML 中,从中断处恢复,这才是更高效的做法。
适用场景: 内容密集型网站、首屏性能严苛的项目、边缘部署 不适用场景: 复杂客户端交互应用、生态重度依赖项目
学习建议: 先理解"可恢复性"和"水合"的区别 → 通过官方教程创建项目 → 构建一个小型应用 → 深入了解 $ 系统