从 instant.page 迁移到 Speculation Rules API
之前本站用 instant.page 优化站内跳转。它的工作方式很好理解:用户 hover 一个链接时,脚本插入 <link rel="prefetch" as="document">,先把下一页 HTML 下载回来。等用户真的点击,浏览器少等一次网络请求,页面就显得更快。
这个方案轻巧,也可靠。只是它仍然是一段第三方脚本,而且它做的事情本质上停在 prefetch:提前拿到目标文档,不能提前把页面跑起来。
现在 Chromium 系浏览器提供了更原生的选择:Speculation Rules API。它允许我们用一段 JSON 告诉浏览器:哪些页面可能会被访问,应该提前准备到什么程度。
适用边界
Speculation Rules API 面向的是 document navigation,也就是从一个 HTML 文档导航到另一个 HTML 文档。它尤其适合 MPA、静态站、内容站、商品列表到详情页这类页面跳转。
它不是通用的资源预加载工具。MDN 文档里专门提醒过:Speculation Rules API 不负责预取页面里的子资源;如果你想提前加载某个 JS chunk、字体或图片,那仍然是 <link rel="prefetch">、preload 或框架自己的资源调度问题。
它也不是强制命令。浏览器会把规则当成一个提示,然后根据网络、内存、电量、用户设置和自身启发式决定要不要执行。不支持的浏览器会忽略 <script type="speculationrules">,所以它适合作为渐进增强。
prefetch 和 prerender
Speculation Rules 里最常用的两种动作是 prefetch 和 prerender。动作 浏览器会做什么 成本 适合什么情况 prefetch提前请求目标文档,不加载目标页子资源,不执行目标页脚本 低 想安全替代 instant.page prerender在后台加载、渲染目标页,包含子资源、脚本执行和脚本触发的数据请求 高 目标页很可能被访问,并且副作用可控
从表格可以看出, prerender 优化更激进:浏览器会在后台准备一个隐藏页面,目标页的 DOM、样式、布局、脚本都可能已经跑过。用户点击时,浏览器可以直接后台页面激活到前台。如果后台页面还没完全准备好,也能带着已经完成的部分继续加载。
公开案例能说明它的上限。web.dev 上有几个相关数据:Ray-Ban 使用 prerender 后,移动端 LCP 从 4.69 秒降到 2.66 秒;Monrif 的桌面 LCP 改善了 17.9%,用户参与度提升 8.9%。[1] 这些数字不代表每个博客都能复现同样收益,但足够说明它在合适场景下确实有实际价值。
博客站点的规则示例
如果只是替代 instant.page,prefetch 已经够用。本站的情况是:静态博客的站内链接状态简单,可以把一部分普通文章跳转交给 prerender。
一个克制的起点可以这样写:<script type="speculationrules"> { "prerender": [ { "where": { "and": [ { "href_matches": "/*" }, { "not": { "selector_matches": "a[href^='#']" } }, { "not": { "selector_matches": "a[href*='?']" } }, { "not": { "selector_matches": "a[target='_blank']" } }, { "not": { "selector_matches": "a[download]" } }, { "not": { "selector_matches": "a[rel~='nofollow']" } }, { "not": { "selector_matches": "a[data-no-prerender]" } } ] }, "eagerness": "moderate" } ] }</script>
如果站点比较复杂,把上面的 "prerender" 改成 "prefetch" ,先用浏览器原生能力替掉 instant.page,再考虑是不是要上 prerender,是更稳妥的迁移顺序。
eagerness 怎么选
eagerness 控制浏览器什么时候开始预测加载。它不是“越积极越好”,而是在命中率和资源浪费之间找一个位置。值 大致触发时机 我的理解 immediate规则被观察到后尽快开始 只适合极少数几乎必点的入口 eager比 moderate 更早适合少量高概率链接,不适合全站铺开 moderate桌面端通常是 hover 约 200ms,或更早的 pointerdown 普通内容站较均衡的起点 conservative接近实际按下鼠标或触屏时 最省资源,收益也最有限
移动端没有 hover,Chrome 会用触摸和视口相关的启发式来判断意图,所以不要把 moderate 机械理解成“鼠标悬停 200ms”。这意味着同一套规则在不同设备上会有不同触发策略,浏览器会根据当前环境压制一部分预测加载。
Chrome 也有硬上限,防止站点过度占用用户设备资源。当前文档里的限制是:eagernessPrefetch Prerender immediate50 10 eager / moderate / conservative2,FIFO 2,FIFO
也就是说,像 moderate 这种依赖用户交互的规则,同一时间通常只保留两个候选;新候选进来,旧候选会被取消。再加上 Save-Data、低电量省电模式、内存压力、用户关闭“预加载网页”、某些扩展主动禁止预加载等情况,最终是否执行仍然由浏览器拍板。
本站需要防的副作用
prerender 的风险要按应用类型看。对本站这种个人博客来说,没有登录、登出、购物车、支付、库存这些状态,所以不需要把所有业务系统的坑都搬进来。
本站首先处理的是统计偏差。prerender 会让目标页脚本在后台运行,pageview计数可能在用户真正进入页面前触发。而如果用户最后没有点击这个链接,那么统计值会高于实际值。const whenActivated = new Promise((resolve) => { if (document.prerendering) { // 当前页面还在后台预渲染,等它真正被用户打开后再继续。 document.addEventListener("prerenderingchange", resolve, { once: true }); } else { // 普通导航进入的页面已经可见,可以直接执行后续初始化。 resolve(); }});whenActivated.then(() => { // 这些逻辑只应该在页面真正展示后运行,避免预渲染阶段污染统计。 initAnalytics(); initComments();});
第二个需要注意的问题是额外请求压力。如果对大量链接都使用 immediate,服务端的请求量就会随之增加,尤其当其中包含一些高开销的计算或资源请求时,账单也就跟着来了 🧾
这也是本站选择 moderate的原因:等用户表现出访问意图后再预渲染,资源浪费更少。
prerender 和 Swup/PJAX 的冲突
prerender 的前提是 document navigation,也就是浏览器从一个 HTML 文档切换到另一个 HTML 文档。
而 Swup、PJAX 会拦截链接点击,自己拉取 HTML 并更新页面内容。整个过程中,浏览器眼里始终只有同一个 document,并没有发生真正的页面导航。既然如此,提前准备好的 prerender 页面自然也就无从激活。
因此,本站最终弃用了 Swup,重新回归浏览器原生导航,并借助跨文档 View Transitions API 做了一点轻量优化。目前实现的效果比较简单:页面切换时,导航栏不会随着整页重载而闪烁。
不过这都是让Codex生成的,实现原理一概不知,等研究明白之后,再单独写一篇分析。
验证方法
验证方法难以文字阐述,吃力不讨好。写到这也累了,感兴趣可自行观看视频👇
结论
如果目标只是替代 instant.page,先用 Speculation Rules 的 prefetch 就可以了。如果想用 prerender,要考虑前文的副作用,以及和Swup/PJAX等SPA方案的兼容性。
数据来源:web.dev 的 Ray-Ban 和 Monrif 案例研究,分别见 Ray-Ban case study 和 Monrif case study;访问日期:2026-06-04。 ↩︎