预加载模块

Sérgio Gomes

发布时间:2024 年 11 月 23 日

基于模块的开发在可缓存性方面具有一些真正的优势,可帮助您减少向用户分发的字节数。代码的粒度越细,就越有助于加载流程,因为您可以优先处理应用中的关键代码。

不过,模块依赖项会引入加载问题,因为浏览器需要等待模块加载完毕,才能确定其依赖项。解决此问题的一种方法是预加载依赖项,以便浏览器提前了解所有文件,并使连接保持繁忙状态。

<link rel="preload"> 是一种在浏览器需要资源之前以声明方式提前请求资源的方法。

<head>
  <link rel="preload" as="style" href="critical-styles.css">
  <link rel="preload" as="font" crossorigin type="font/woff2" href="myfont.woff2">
</head>

这对于字体等资源特别有用,因为这些资源通常隐藏在 CSS 文件中,有时深度可达几层。在这种情况下,浏览器必须等待多次往返,才能发现自己需要提取大型字体文件,而在此期间,它本可以开始下载并充分利用连接带宽。

<link rel="preload"> 及其 HTTP 标头等效项提供了一种简单的声明式方法,可让浏览器立即了解当前导航中需要的关键文件。当浏览器看到预加载时,会开始对资源进行高优先级下载,以便在实际需要时,资源已提取或已部分提取。不过,它不适用于模块。

这时情况就变得有点棘手了。资源有几种凭据模式,为了获得缓存命中,它们必须匹配,否则您最终会提取两次资源。不用说,重复提取是一件坏事,因为它会浪费用户的带宽,并让用户无故等待更长时间。

对于 <script><link> 标记,您可以使用 crossorigin 属性设置凭据模式。不过,事实证明,没有 crossorigin 属性的 <script type="module"> 表示凭据模式为 omit,而 <link rel="preload"> 不存在此模式。这意味着,您必须将 <script><link> 中的 crossorigin 属性都更改为其他值之一,如果您尝试预加载的资源是其他模块的依赖项,则可能无法轻松地执行此操作。

此外,提取文件只是实际运行代码的第一步。首先,浏览器必须对其进行解析和编译。理想情况下,这也应提前完成,以便在需要模块时,代码已准备好运行。不过,V8(Chrome 的 JavaScript 引擎)解析和编译模块的方式与其他 JavaScript 不同。<link rel="preload"> 不提供任何指示要加载的文件是模块的方法,因此浏览器只能加载文件并将其放入缓存中。使用 <script type="module"> 标记加载脚本(或由其他模块加载脚本)后,浏览器会将代码解析并编译为 JavaScript 模块。

简而言之,是的。通过为预加载模块使用特定的 link 类型,我们可以编写简单的 HTML,而无需担心所使用的凭据模式。默认设置即可正常运行。

<head>
  <link rel="modulepreload" href="super-critical-stuff.mjs">
</head>
[...]
<script type="module" src="super-critical-stuff.mjs">

由于浏览器现在知道您要预加载的是模块,因此它可以智能地在提取完成后立即解析和编译模块,而不是等到尝试运行时再进行解析和编译。

浏览器支持

  • Chrome:66.
  • Edge:≤79。
  • Firefox:115.
  • Safari:17.

来源

但模块的依赖项呢?

很高兴您问了这个问题!本文确实没有介绍递归。

实际上,<link rel="modulepreload"> 规范允许您选择性地加载请求的模块以及其所有依赖项树。浏览器不必这样做,但可以这样做。

由于您需要完整的依赖项树才能运行应用,因此哪种跨浏览器解决方案最适合预加载模块及其依赖项树?

选择递归预加载依赖项的浏览器应具有强大的模块去重功能,因此一般而言,最佳实践是声明模块及其依赖项的扁平列表,并信任浏览器不会提取同一模块两次。

<head>
  <!-- dog.js imports dog-head.js, which in turn imports
       dog-head-mouth.js, which imports dog-head-mouth-tongue.js. -->
  <link rel="modulepreload" href="dog-head-mouth-tongue.mjs">
  <link rel="modulepreload" href="dog-head-mouth.mjs">
  <link rel="modulepreload" href="dog-head.mjs">
  <link rel="modulepreload" href="dog.mjs">
</head>

预加载模块是否有助于提升性能?

预加载可以告知浏览器需要提取的内容,以免浏览器在这些漫长的往返过程中无所事事,从而最大限度地提高带宽使用率。如果您在实验模块时因依赖项树较深而遇到性能问题,创建预加载的扁平列表肯定会有所帮助。

不过,我们仍在努力提升模块性能,因此请务必使用开发者工具仔细查看应用中发生的情况,同时考虑将应用打包成多个分块。不过,Chrome 中仍有大量模块工作在进行中,因此我们离让捆绑程序获得应有的休息越来越近了!