prefetch 不会真正预渲染下一页,仅静默缓存 HTML 文件;它依赖 window.onload、主线程空闲和带宽余量三条件,且 as="document" 易致白屏、流量浪费与兼容问题;推荐 prefetch 固定路径的 chunk 文件或用 import() 动态触发。
在后台静默加载用户可能访问的下一页文档">
prefetch 会静默加载下一页文档吗
会,但不是你想象的那种“加载”。rel="prefetch" 只把目标 HTML 文件存进 HTTP 缓存,不执行 JS、不解析 DOM、不恢复表单状态、不预渲染。用户点进去后,浏览器仍要重新 fetch(若缓存失效)、执行 script、挂载组件、拉接口——它只省了一次网络下载,仅此而已。
更关键的是:这个“静默加载”极不可控。浏览器必须同时满足三个硬条件才可能发起请求:window.onload 已触发、主线程空闲、网络带宽有低优先级余量。多步表单里 JS 校验卡住 onload,或用户秒点“下一步”,prefetch 就永远不会启动。
为什么直接写 <link rel="prefetch" href="/next-page.html" as="document"> 很危险
- as="document" 会连带拉取该 HTML 中所有 <script>、<link rel="stylesheet">、图片,实际流量常达 1–2 MB,弱网下几秒耗尽用户套餐
- 服务端返回的 HTML 若含动态 nonce、CSRF token 或时间戳,缓存后下次导航会执行失效代码,直接白屏
- Safari 在 iOS 16.4 之前基本忽略 as="document",Chrome Network 面板默认不显示 prefetch 请求,排查困难
- 跨域请求静默失败,连 DNS 查询都不会触发,Network 里什么也看不到
真正能用的写法:只 prefetch 确定路径的 chunk 文件
- 目标必须是构建产物中固定不变的资源路径,比如 Vite 输出的 /assets/DetailPage.abc123.js,而不是路由地址 /detail
- 显式声明 as="script"(JS)或 as="style"(CSS),绝不写 as="js" 或 as="fetch" —— 写错就静默失效
- 对应 CSS 分离时,必须额外加一行 <link rel="prefetch" href="/assets/DetailPage.def456.css" as="style">
- 服务端响应头必须含 Cache-Control: public, max-age=31536000,否则缓存立即失效
- 放在 HTML 的 <head> 中且初始解析时就存在,动态插入(如 document.head.appendChild())90% 无效
比静态 prefetch 更可靠的做法:用 import() 或行为触发
- 如果“下一页”已做路由懒加载(如 React 的 lazy(() => import('./DetailPage'))),直接调用该 import() 表达式 —— 浏览器自动预取对应 chunk,优先级略高,且全平台兼容
- 用户鼠标悬停卡片时,延迟 200ms 后调用:const link = document.createElement('link');<br>link.rel = 'prefetch';<br>link.as = 'script';<br>link.href = '/assets/DetailPage.abc123.js';<br>document.head.appendChild(link);
- 滚动到可视区域再触发,配合 navigator.onLine 和 saveData 判断是否启用(避免省流模式下触发)
/assets/DetailPage.abc123.js,而不是路由地址 /detail
- 显式声明 as="script"(JS)或 as="style"(CSS),绝不写 as="js" 或 as="fetch" —— 写错就静默失效
- 对应 CSS 分离时,必须额外加一行 <link rel="prefetch" href="/assets/DetailPage.def456.css" as="style">
- 服务端响应头必须含 Cache-Control: public, max-age=31536000,否则缓存立即失效
- 放在 HTML 的 <head> 中且初始解析时就存在,动态插入(如 document.head.appendChild())90% 无效
比静态 prefetch 更可靠的做法:用 import() 或行为触发
- 如果“下一页”已做路由懒加载(如 React 的 lazy(() => import('./DetailPage'))),直接调用该 import() 表达式 —— 浏览器自动预取对应 chunk,优先级略高,且全平台兼容
- 用户鼠标悬停卡片时,延迟 200ms 后调用:const link = document.createElement('link');<br>link.rel = 'prefetch';<br>link.as = 'script';<br>link.href = '/assets/DetailPage.abc123.js';<br>document.head.appendChild(link);
- 滚动到可视区域再触发,配合 navigator.onLine 和 saveData 判断是否启用(避免省流模式下触发)
复杂点在于:它不提供任何回调、不报告失败、不暴露缓存状态。你写了它,只是往 HTTP 缓存里扔了个文件;用户点进去之后发生的一切,它一概不管。

















