1. 为什么关注"零运行时"(Zero/Minimal JS Runtime)与极致懒加载
痛点
- 首屏 JS 体积与执行时间(Parse/Compile/Execute)过大,移动端受限更明显。
- 传统 SSR + Hydration:整页级别注水,哪怕用户只与局部交互,也需要恢复整页组件树的事件与状态。
- 构建时代码分拆(split)不等于运行时按需:路由级分包仍会加载大量非必要逻辑。
目标
- 以用户交互为中心的懒执行:不交互就不下载、不解析、不执行。
- 最小可用 JS:运行时尽可能小,避免为框架本身支付额外成本。
- 直达交互点 :状态与事件可以从 HTML 中直接恢复(可恢复性 Resumability),跳过全树 Hydration。
2. 核心理念对比:SolidJS vs Qwik
维度 | SolidJS | Qwik |
---|---|---|
运行时 | 极小、编译期优化 + 细粒度响应式,最小必要运行时 | 追求接近零运行时 首屏执行;通过 Resumability 实现按需恢复 |
响应模型 | Signals + fine-grained reactivity(无虚拟 DOM diff) | Signals + Resumable closure,事件与状态可从 HTML 直接恢复 |
Hydration | 传统意义上仍需对交互区进行恢复,但粒度很细 | 跳过 Hydration,用户交互时才懒恢复相关岛屿代码 |
代码分拆 | 标准动态 import + 路由级 + 组件级按需 | qrl(Qwik URLs) 将闭包序列化,到点下载、到点执行 |
生态 | 与 React 写法相近,学习成本低,兼容 TS/Vite | 专属 Qwik City(路由/SSR),理念更前沿,收益极致 |
3. 关键技术点
3.1 Signals(信号)
- 写法 :
const [count, setCount] = createSignal(0)
(SolidJS);const count = useSignal(0)
(Qwik) - 优势:订阅粒度到具体表达式,不再对整颗组件树 diff;计算属性只在依赖变化时重算。
3.2 Fine-grained Reactivity(细粒度反应)
- 由编译器/运行时建立精确依赖图,避免 VDOM diff 抖动。
- SolidJS:以"signal → computation"图驱动更新。
3.3 Partial Hydration(局部注水)与 Islands 架构
- 将页面拆成多个"岛",仅为交互岛注水。
- 限制 :仍需在客户端下载与执行岛的 JS 以完成 Hydration。
3.4 Resumability(可恢复性,Qwik)
- 将组件闭包与事件处理器序列化为 QRL(URL) ,把状态存在 DOM/HTML 中。
- 首次交互时,框架定位目标处理器 ,只下载那一小段代码并执行。
- 避免整树 Hydration,首屏接近零 JS 执行。
4. SolidJS 实战:从 0 到 1
目标:构建一个"可交互排行榜"页面,首屏渲染快、更新粒度细。
4.1 初始化
使用 Vite 模板
js
npm create vite@latest solid-demo -- --template solid-ts
cd solid-demo
npm i
npm run dev
4.2 Signals 与计算属性
js
// src/components/Counter.tsx
import { createSignal, createMemo } from "solid-js";
export default function Counter() {
const [count, setCount] = createSignal(0);
const doubled = createMemo(() => count() * 2);
return (
<div>
<p>count: {count()} / doubled: {doubled()}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
);
}
4.3 懒加载组件(基于路由/交互)
js
// src/routes/index.tsx
import { lazy } from "solid-js";
const HeavyChart = lazy(() => import("../components/HeavyChart"));
export default function Home() {
const [showChart, setShowChart] = createSignal(false);
return (
<main>
<h1>SolidJS Zero-Overhead Demo</h1>
<button onClick={() => setShowChart(true)}>加载图表</button>
{showChart() && <HeavyChart />}
</main>
);
}
js
import { onMount } from "solid-js";s
export default function HeavyChart() {
onMount(async () => {
// 在组件内部再做二次懒加载(如第三方库)
const { Chart } = await import("chart.js");
// ...init chart
});
return <div id="chart" style={{ height: "240px" }} />;
}
4.4 SSR 与数据获取(可选)
- SolidStart(Solid 官方应用框架)支持 SSR/路由/数据请求:
js
npm create solid@latest solid-start-app
// src/routes/index.tsx(SolidStart)
import { createRouteData } from "solid-start";
export function routeData() {
return createRouteData(async () => {
const res = await fetch("/api/toplist");
return res.json();
});
}
export default function Page() {
const data = routeData();
return (
<ul>
{data()?.list.map((item: any) => (
<li>{item.name} - {item.score}</li>
))}
</ul>
);
}
要点:Solid 的最小运行时 + 懒加载策略,能显著降低非必要 JS 的解析与执行成本。
5. Qwik 实战:从 0 到 1(含 Resumability)
目标:同样实现"可交互排行榜 + 重组件懒加载",但首屏不执行多余 JS,通过 Resumability 到点恢复。
5.1 初始化(Qwik City)
js
npm create qwik@latest
选择 Qwik City + SSR
js
cd qwik-app
npm i
npm run dev
5.2 Signals 与服务器端数据
js
// src/routes/index.tsx
import { component$, useSignal, useVisibleTask$, routeLoader$ } from "@builder.io/qwik";
export const useTopList = routeLoader$(async () => {
const list = await fetch("/api/toplist").then(r => r.json());
return list;
});
export default component$(() => {
const list = useTopList();
const showChart = useSignal(false);
return (
<main>
<h1>Qwik Resumability Demo</h1>
<button onClick$={() => (showChart.value = true)}>加载图表</button>
{showChart.value && <HeavyChart />}
<ul>
{list.value?.list?.map((it: any) => (
<li>{it.name} - {it.score}</li>
))}
</ul>
</main>
);
});
5.3 事件处理器与懒恢复(onClick$
)
- 注意
onClick$
:Qwik 会把该处理器序列化为 QRL,首屏仅在 HTML 中标注链接。 - 用户点击时,才下载执行对应模块,无需整页 Hydration。
5.4 重组件按需下载(lazy
via routeLoader$
+ 动态 import)
js
// src/components/HeavyChart.tsx
import { component$, useVisibleTask$ } from "@builder.io/qwik";
export default component$(() => {
useVisibleTask$(async () => {
const { Chart } = await import("chart.js");
// 初始化图表逻辑...
});
return <div id="chart" style={{ height: "240px" }} />;
});
useVisibleTask$
:仅当组件在视口可见时才运行;结合滚动/折叠区可以实现更晚的懒执行。
5.5 表单与行动(Action$)在边缘运行(可选)
js
// src/routes/actions.tsx
import { component$, action$, z, Form } from "@builder.io/qwik-city";
export const useSubmitScore = action$(async ({ name, score }) => {
// 运行在 server/edge,最小客户端 JS
// ...持久化逻辑
return { ok: true };
});
export default component$(() => {
const submit = useSubmitScore();
return (
<Form action={submit}>
<input name="name" />
<input name="score" type="number" />
<button type="submit">提交</button>
{submit.value?.ok && <p>提交成功</p>}
</Form>
);
});
要点:Qwik 的 Resumability 使首屏近似"零 JS 执行",用户交互到哪就只恢复哪。
6. 懒加载策略与代码分拆清单
6.1 策略清单
- 路由级 split:按页面维度拆包。
- 组件级 lazy :对重组件(图表、编辑器、地图)做
lazy(() => import(...))
。 - 可见即执行 :
useVisibleTask$
(Qwik) / 观察器(IntersectionObserver)延后到可见时。 - 交互触发加载:按钮点击后再下载(示例中的 Chart.js)。
- 数据延迟获取 :首屏仅渲染必要骨架,真实数据在空闲时(
requestIdleCallback
)或可见时获取。 - 第三方 SDK 延时:社交/广告/监控 SDK 放在交互或后台空闲执行。
6.2 代码片段:通用可见懒执行(Solid 示例)
js
import { onCleanup, onMount } from "solid-js";
export function onVisible(el: HTMLElement, cb: () => void) {
const io = new IntersectionObserver((entries) => {
entries.forEach((e) => {
if (e.isIntersecting) {
cb();
io.disconnect();
}
});
});
io.observe(el);
onCleanup(() => io.disconnect());
}
// 用法
// <div ref={el => onVisible(el, () => importHeavy()))} />
6.3 代码片段:按需加载第三方库(两端通用思想)
js
async function loadLib<T = any>(url: string, globalVar: string): Promise<T> {
if ((window as any)[globalVar]) return (window as any)[globalVar];
await new Promise<void>((resolve, reject) => {
const s = document.createElement('script');
s.src = url; s.async = true;
s.onload = () => resolve();
s.onerror = reject;
document.head.appendChild(s);
});
return (window as any)[globalVar] as T;
}
7. 总结与选型建议
- 追求极致首屏 、大量内容站点、弱交互页面 → Qwik(Resumability) 更有优势。
- 需要细粒度响应 + 运行时极小 ,又想快速上手 → SolidJS 是务实之选。
- 不必二选一:Solid 用于复杂交互区,Qwik 用于内容站,或分项目落地。