2026 年 10 月 25 日,我让 AI coding agent 把 aron.tw 从 Next.js 改成 SvelteKit,上线后又改成 Astro。读者看到的网站大致一样,搜索、分类、语言切换也都还在。搜索页的外部 JavaScript 却从 155.7 KB 降到 15.7 KB。
这是这次重构最直接的收获。网站没有少掉我想保留的功能,浏览器需要下载和运行的程序少了很多。
我想藉这个项目看看,一个主要供人读文章的网站,换掉运行模型之后,能省下多少工作,又会留下什么。当时网站有五种语言、35 份文章语言版本、约 60 个静态页面,部署在 Cloudflare Pages。交互包括 Pagefind 搜索、分类筛选、复制按钮、主题与语言菜单,以及首页的人像滑块。
这些交互可以一块一块分开处理。搜索条件改变,不需要连带更新文章页的菜单;读完一篇换到下一篇,也没有一份编辑到一半的表单要跟着走。这是我愿意试 Astro 的重要条件。
迁移沿用既有的行为测试。Astro 与 SvelteKit 的 210 组静态及交互截屏,比对结果是零像素差异;约 223 个 E2E 的验收规格则保留下来,调整少数与框架实现绑定的案例。这些记录让我能把「功能有没有留下来」和性能数字分开检查。
HTML 已经有了,浏览器还在忙什么?
三版都是静态部署,都会送出 HTML。差别在 HTML 之后。
Next.js 在构建时运行 Server Components,除了 HTML,也准备客户端换页使用的 payload;Client Components 需要 hydration 接上交互。SvelteKit 同样会 hydrate 页面,并由 client router 接手站内换页。这些机制让组件状态与导航能在浏览器里延续。Next.js 静态导出、SvelteKit 页面选项
本站的 Astro 版把文章在构建时处理完,菜单、筛选等功能交给普通 TypeScript 脚本。没有 React 或 Svelte 在浏览器里接手整页,换页则加载下一份 HTML。
搜索页最适合拿来对照,因为三版都包含同一套 Pagefind,没有把搜索功能拿掉来换小数字。
搜索页的外部 JavaScript
这份 Astro 实现比原 Next.js 版少了约 90% 的外部 JS。
- Next.js155.7
- SvelteKit60.6
- Astro15.7
查看搜索页 JS 数据
| 框架 | KB |
|---|---|
| Next.js | 155.7 |
| SvelteKit | 60.6 |
| Astro | 15.7 |
本机同轮比较,数值为 Brotli 压缩后的外部 Script response body,三版皆含 Pagefind;不含 HTML 内嵌脚本,因此不是全部 JS 传输量。取保存的首笔样本,KB 以 1,024 bytes 计算。
测量摘录与方法一般阅读页的 Astro 外部 JS 更少,这轮各页约为 0.9–3.2 KB,搜索页另计。不过,Astro 也把部分小脚本内嵌在 HTML 里,不能看见很小的外部文件,就把它理解成没有 JavaScript。
浏览器运行的工作也有减少。这轮 Chromium 测试使用 390 px 移动端视口、4 倍 CPU 降速,各页跑五次取中位数:
| 各页中位数的范围 | Next.js | SvelteKit | Astro |
|---|---|---|---|
| Script 运行时间 | 47–88 ms | 14–35 ms | 4–14 ms |
| JS heap | 3.1–4.2 MB | 1.6–2.6 MB | 0.8–0.9 MB |
范围来自不同页面,并非同一页的误差区间;heap 也只描述 JavaScript 内存。对这个博客来说,少送程序、少运行程序、少保留 JS 内存,都是值得留下的改善。
Lighthouse 的分数却几乎没动。另一组 Lighthouse 13.5.0 移动端模拟中,每页三次取中位数,FCP 仍约 3–12 秒;三版共同加载的 11 个同步字体 stylesheet,仍卡在首次绘制的路径上。
因此下一步若要让文本更早出现,我会去处理字体。框架已经省掉一部分工作,剩下的瓶颈也变得很具体,没有必要再换一次框架来回答同一个问题。
交互没有消失,只是换了写法
SvelteKit 那一段迁移,留下两个很有用的案例。
第一个是文章已经在 HTML 里,hydration payload 又带了一份。项目最后搭配 transport hook 和自己的输出处理,把首次加载的重复内文移掉,同时保留客户端换页需要的数据。这是本站额外做的处理,不是关掉一个选项就自动完成。
第二个是 head 里约 18 个高优先级 modulepreload。在当时的慢速 4G 模拟里,它们与字体竞争带宽,让 FCP 多了 0.6–0.9 秒;调成 fetchpriority="low" 后才回到原来的水准。bundle 小了,加载顺序仍然会影响读者先看见什么。
Astro 版不用重建这套 hydration 状态,文章 payload 的重复问题也就不必再处理。但搜索与分类筛选还是要有人写。
这次的做法是把文章卡片先渲染进 <template>。筛选条件改变时,脚本复制符合条件的卡片、重排列表、更新分页和网址。对话框、菜单和人像滑块也各自有脚本。迁移完成时,这些交互共八个 TypeScript 模块、860 行。
迁移记录里还有一个很实际的问题:组件脚本曾随 <template> 里的内容输出,留在不会自动运行的模板中。当时把菜单脚本移到外层加载,才让事件确实接上。这类问题会让人重新注意到脚本放在哪里;没有 hydration,也还是得把这些位置安排好。
我接受这些代码,因为目前每块交互都能分开理解。将来若搜索开始包含编辑、跨区块联动或大量共享状态,我不会只为了维持「纯 TypeScript」就继续手动同步所有 DOM。Astro 仍能在需要的地方放进 Svelte 或 React island,到时再比较那一块的复杂度。
冷构建也短了,但暖构建是另一回事
浏览器以外,冷构建从 5.81 秒缩到 2.55 秒,也是这次确实量到的收益。
同一网站的冷构建时间
这轮 Astro 冷构建比原 Next.js 实现约少 56%。
- Next.js5.81
- SvelteKit3.88
- Astro2.55
查看冷构建数据
| 框架 | 秒 |
|---|---|
| Next.js | 5.81 |
| SvelteKit | 3.88 |
| Astro | 2.55 |
Apple M3 Max,同轮交错、各三次中位数。冷构建清除框架缓存与输出,保留共用 OG 图缓存;计入内容处理与 Pagefind,并非纯框架编译时间。
测量摘录与方法如果接着再构建一次,Next.js 的持久缓存会缩小差距。这轮暖构建为 Next.js 2.98 秒、SvelteKit 4.07 秒、Astro 2.63 秒。冷构建的改善可以成立,也不用把它延伸成任何构建条件下都快一倍。
这里我没有再用代码行数或包数替框架排名。三份实现交给框架与自己维护的工作不同,行数不能直接换算成工时。860 行交互程序值不值得继续自己管,最后还是要在后续修改时才知道。
换页那组数字,需要把解读改回来
这次记录里,换页比外部 JS 更容易读错。
本机从博客列表点进文章,三版的流程时间如下:
另一组正式站记录则是 SvelteKit 373 ms、Astro 768 ms,各跑三次取中位数。这个差距曾让换页成为讨论重点,但两份数据来自不同部署时段,CDN 与网络状态不同,测试等待的生命周期也不同。Playwright 的 locator.click() 会等待它触发的导航,完整文档加载和 client router 更新画面不能直接当成相同终点。
原始记录还曾把约 50 个资源事件解释成大量 304 验证。这个解释需要更正:requestfinished 也会计入从 browser cache 取出的资源。没有逐条响应与缓存来源,光靠事件数,算不出多少次网络往返。
2026 年 10 月 25 日保存的正式站核对结果,是 HTML 采 max-age=0, must-revalidate,字体与抽查图片有四小时 freshness,带 hash 的 /_astro/ CSS、JS 则有一年 immutable。四小时内仍新鲜的字体,不会只因为有 must-revalidate 就每次都上网验证;这个指令约束的是缓存过期后的重用。HTTP 缓存规格
所以,完整文档导航确实要重新解析 HTML、创建 DOM,CSS、字体与图片却可以从浏览器缓存拿。CDN 命中又是另一件事:它让响应来自更靠近用户的节点,浏览器仍然要发出请求。要看换页卡在哪里,这几种情况必须分开。
本站也开了 hover prefetch。依 Astro 7.4.2 的实现,支持时会用 <link rel="prefetch"> 提前抓下一份 HTML;这不等于下一页所有资源和交互都已经准备好。当时光标停留 300 ms 的测试没有改善流程时间,只能描述那次测试,不能据此判定 prefetch 没有用途。
若接下来要比较换页体验,我会重新固定冷/暖 browser cache、确认预加载是否完成,再以相同的可见内容时间点测量。现有数据已经足够确认 JS 与构建的改善;换页的问题值得另外做对。
这个博客,我会留在 Astro
这次的网站以阅读为主,交互也能局部分开。Astro 让页面在构建时完成,浏览器只接手需要的控制项,这和我目前要维护的东西很接近。
我不会把这个选择直接搬到每个项目。若页面之间要保留大量操作状态,或好几块界面经常一起更新,Svelte 的响应式组件与客户端导航就有具体用途。SvelteKit 也能用 csr=false 关闭 hydration 与 client router,但原本依赖 Svelte 的交互必须另行安排。
已有 Next.js 项目,则可以先检查 client boundary 与路由分包。本站没测 Server Actions、流式处理等能力,这次重写也没有证明换框架比整理原本的实现更划算。AI 和既有测试让我有条件做这次实验,并不代表每个团队都值得投入同样的迁移。
对 aron.tw,现在的选择已经够清楚了:留下 Astro,把那几块交互维护好,再处理仍挡着文本出现的字体。框架先选到这里,网站还有别的事要做。
