用 Astro islands 把首屏 JS 压到零

从「异步引入」到「按需水合」,讲清楚三层懒加载分别解决什么问题,并给出这个站点里真实在跑的代码。

「异步引入」这个词其实很含糊。它至少可以指三件不同的事,而它们的收益差了一个数量级。

第一层:路由级代码分割

最基础的一层。打包器把每个页面切成独立的 chunk,访问 /games/snake 时只下载蛇的代码,不会把 2048 一起拖下来。

Vite 里用 import.meta.glob 就够了:

const loaders = {
  ...import.meta.glob('../games/*/App.vue'),
  ...import.meta.glob('../tools/*/App.vue'),
};

关键在于这个 glob 必须能被静态分析。写得太动态(比如把路径拼成变量)打包器就只能整个打包,切分就失效了。

第二层:水合级延迟

这一层是 Astro 独有的,也是它真正比文档框架强的地方。

在 VitePress 里,defineAsyncComponent 会在组件渲染时立刻去下载。列表页放二十张卡片,就是二十个 chunk 同时飞出去。而 Astro 可以这样写:

<GameShell client:visible />

client:visible 的意思是:进入视口之前,这个组件连代码都不下载。除此之外还有 client:idle(浏览器空闲)、client:media(匹配媒体查询)、client:only(跳过 SSR)。

下面这个小东西就是真的走了这条路径 —— 它现在还没被水合,你往下滚到它出现才会加载:

7x2 = 14

第三层:运行时按需

前两层解决的是「什么时候下载」,这一层解决的是「要不要下载」。

游戏引擎、图表库、Web Worker 里的解码器,这些都不该出现在任何首屏包里。JSON 工作台里的「对比」模式就是这么做的 —— 只有你真的点了那个按钮,才会去 import 差异算法:

const mod = await import('./logic/diff');
diffEntries.value = mod.diffValues(left.value, right.value);

收尾

三层加起来的效果是:内容页几乎零 JS,应用页按应用切分,重依赖再按操作切分。

代价是构建配置和心智模型都更复杂了 —— 你得同时理解 SSG、islands 和客户端挂载三套东西。但如果站点里同时有文章和游戏,我认为这个代价是值的。

← 返回文章列表

评论

…