发布时间:2024 年 11 月 23 日
基于模块的开发在可缓存性方面提供了一些真正的优势,有助于减少您需要交付给用户的字节数。代码的更精细粒度也有助于加载故事,让您可以优先处理应用程序中的关键代码。
但是,模块依赖关系引入了一个加载问题,即浏览器需要等待模块加载后才能找出其依赖关系。解决此问题的一种方法是预加载依赖项,以便浏览器提前知道所有文件,并可以保持连接繁忙。
<link rel="preload">
<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 标头等效项提供了一种简单、声明式的方式,让浏览器立即了解作为当前导航一部分所需的关键文件。当浏览器看到预加载时,它会启动资源的高优先级下载,以便在实际需要时,资源要么已经获取,要么部分可用。但是,它不适用于模块。
为什么 <link rel="preload">
不适用于模块?
这就是事情变得棘手的地方。资源有几种凭据模式,为了获得缓存命中,它们必须匹配,否则您最终会两次获取资源。不用说,双重获取是不好的,因为它浪费了用户的带宽,并让他们等待更长时间,没有任何正当理由。
对于 <script>
和 <link>
标签,您可以使用 crossorigin
属性设置凭据模式。但是,事实证明,没有 crossorigin
属性的 <script type="module">
指示 omit
的凭据模式,这对于 <link rel="preload">
不存在。这意味着您必须将 crossorigin
属性更改为 <script>
和 <link>
中的其他值之一,如果您尝试预加载的是其他模块的依赖项,您可能没有简单的方法来做到这一点。
此外,获取文件只是实际运行代码的第一步。首先,浏览器必须解析和编译它。理想情况下,这也应该提前发生,以便在需要模块时,代码已准备好运行。但是,V8(Chrome 的 JavaScript 引擎)解析和编译模块的方式与其他 JavaScript 不同。<link rel="preload">
没有提供任何指示正在加载的文件是模块的方法,因此浏览器可以做的就是加载文件并将其放入缓存中。一旦使用 <script type="module">
标记加载脚本(或者它由另一个模块加载),浏览器就会将代码解析并编译为 JavaScript 模块。
那么 <link rel="modulepreload">
只是模块的 <link rel="preload">
吗?
简而言之,是的。通过为预加载模块设置特定的 link
类型,我们可以编写简单的 HTML,而无需担心我们正在使用什么凭据模式。默认值就可以正常工作。
<head>
<link rel="modulepreload" href="super-critical-stuff.mjs">
</head>
[...]
<script type="module" src="super-critical-stuff.mjs">
并且由于浏览器现在知道您预加载的是模块,因此它可以很智能,并在完成获取后立即解析和编译模块,而不是等到尝试运行时才进行。
但是模块的依赖项呢?
您问得正好!本文确实遗漏了一些内容:递归。
<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 正在进行大量模块工作,因此我们越来越接近让打包器好好休息一下了!