preload、prefetch、preconnect、dns-prefetch 和 modulepreload 有什么区别?
dns-prefetch 和 preconnect 只为跨域连接做准备,一个只查 DNS,一个连 TCP 和 TLS 也建好;preload 高优先级下载当前页面马上要用的资源;modulepreload 下载 ES 模块并提前解析编译,放进模块映射;prefetch 低优先级下载将来导航可能用到的资源,放进 HTTP 缓存。
简答
这五个都写在
<link rel="...">里,告诉浏览器提前做点事。区分它们只要问两个问题:提前做到哪一步,为谁做。
dns-prefetch和preconnect只管连接,不下载任何文件。dns-prefetch只提前查好域名,preconnect把 DNS、TCP 和 TLS 握手全做完。它们只对跨域的源有用,因为同源的连接已经开着了。另外三个会真的下载文件。
preload用于当前页面很快就要用、但浏览器发现得晚的资源,比如 CSS 里引用的字体,它以较高优先级下载并缓存,但不执行。modulepreload是给 ES 模块用的preload,下载后还会解析、编译,放进文档的模块映射,等着执行。prefetch是为将来准备的,比如用户很可能点开的下一个页面要用的文件,它以很低的优先级下载,结果放进 HTTP 缓存。要注意,这些都会占用带宽和连接,MDN 反复提醒不要什么都预加载。
preload字体要加crossorigin;prefetch受缓存分区影响,不能给别的顶层站点预取;跨站文档的预取更推荐用 Speculation Rules API。
先看写法
<!-- 只查 DNS -->
<link rel="dns-prefetch" href="https://cdn.example" />
<!-- 建好连接,字体这类匿名 CORS 请求要加 crossorigin -->
<link rel="preconnect" href="https://fonts.example" crossorigin />
<!-- 当前页面要用的字体 -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin />
<!-- 当前页面要用的 ES 模块 -->
<link rel="modulepreload" href="/js/app.js" />
<!-- 下一页可能用到的资源 -->
<link rel="prefetch" href="/js/checkout.js" />
只建连接:dns-prefetch 和 preconnect
浏览器请求一个新的源,要先查 DNS,再建 TCP 连接,HTTPS 还要做 TLS 握手,这几步都要等往返。preconnect 把这几步提前做完,dns-prefetch 只做第一步。
既然 preconnect 做得更多,为什么还要 dns-prefetch?因为一条打开但没用上的连接也在占资源。MDN 的建议是:preconnect 只留给最关键的几个跨域源,其他的用 dns-prefetch 省下 DNS 查询的时间就够了。两个也可以对同一个源一起写,不支持 preconnect 的浏览器还能退回到 dns-prefetch。
crossorigin 为什么会影响 preconnect?字体这类资源是用匿名模式的 CORS 请求加载的。MDN 的说明是,这种情况下 preconnect 要加 crossorigin,不加的话浏览器只会做 DNS 查询。这几个提示怎么选,另见《dns-prefetch、preconnect 和 preload 应该怎样选择?》。
当前页面要用:preload 和 modulepreload
preload 解决的是“发现得晚”。HTML 里直接写着的 <link rel="stylesheet"> 和 <script> 浏览器很快就能看到,preload 帮不了多少。真正有用的是藏在后面的资源,比如 CSS 里 @font-face 引用的字体、JS 运行后才请求的文件。
as 属性必须写。MDN 列出的理由是,浏览器要靠它把资源存进缓存供后面的请求复用,套用正确的 CSP,并设置正确的 Accept 请求头。字体和 fetch 类的预加载还要加 crossorigin,让预加载的 CORS 模式和真实请求一致,否则可能白下一次。
还有一个容易踩的坑。<picture> 里给 AVIF 和 WebP 都写了 preload,同时支持两种格式的浏览器会把两个都下载,可最终只用一个。MDN 建议只给大多数用户会用到的那种格式写 preload。
modulepreload 是专为 ES 模块设计的。普通 preload 只下载并放进缓存;modulepreload 下载后还会解析、编译,把结果放进模块映射,等着执行。它的请求模式总是 cors。为什么值得单独用?因为模块依赖要等父模块下载、解析完才能发现,层层串行。把依赖都写成 modulepreload,它们就能并行下载。不过浏览器是否会顺带预取依赖的依赖,规范没有强制,所以 MDN 提示想要所有浏览器都预加载,就得逐个写出来。
将来才用:prefetch
prefetch 告诉浏览器,用户之后的导航很可能用到这个资源。它的优先级比 preload 低,请求会带上 Sec-Purpose: prefetch 头,结果放进 HTTP 缓存,所以当前页面用不到的子资源也可以预取。
它有两个限制。第一,结果存在 HTTP 缓存里,响应头比如 Cache-Control: no-cache 会让预取失效。第二,很多浏览器对 HTTP 缓存做了分区,给别的顶层站点预取的资源,到了那个站点上用不了。所以 MDN 建议预取下一个页面本身的 HTML 时,优先用 Speculation Rules API,见《Speculation Rules API 的预取和预渲染有什么区别?》。
常见追问
preload 了一个资源,页面没用上会怎样?
白白占了带宽,还可能挤占真正关键的请求。MDN 对
preload的建议是只用在高优先级的资源上,不要什么都预加载,否则就看不到效果。
这些提示能写在响应头里吗?
可以,用
Link响应头,比如Link: <https://example.com>; rel="preconnect"。好处是浏览器收到响应头就能开始准备,不用等 HTML 解析到<head>。
webpack、Vite 里的 preload 和这个有关系吗?
有关系。以 Vite 为例,它的
build.modulePreload选项控制怎样预加载动态导入依赖的 chunk:每个动态导入要预加载哪些 chunk 由 Vite 计算,默认还会注入一段 module preload 的 polyfill。构建工具做的是根据打包结果替你安排这些预加载,最终用的仍是浏览器的资源提示机制。