了解浏览器预加载扫描器是什么、如何帮助提升性能,以及如何避免受到它的影响。
优化网页速度的一个容易被忽视的方面是,需要了解一些浏览器内部结构。浏览器会进行某些优化来提升性能,而这些优化是开发者无法实现的,但前提是这些优化不会被意外阻止。
需要了解的一项内部浏览器优化是浏览器预加载扫描器。本文将介绍预加载扫描器的工作原理,更重要的是,介绍如何避免妨碍其正常运行。
什么是预加载扫描器?
每个浏览器都有一个主要 HTML 解析器,用于将原始标记标记化并将其处理为对象模型。在解析器发现阻塞资源(例如使用 <link> 元素加载的样式表,或使用 <script> 元素加载的没有 async 或 defer 属性的脚本)时,解析器会暂停,在此之前一切都正常进行。
<link> 元素,这会阻止浏览器解析文档的其余部分(甚至阻止浏览器呈现任何部分),直到 CSS 下载并解析完毕。
对于 CSS 文件,系统会阻止渲染,以防止出现无样式内容闪烁 (FOUC),即在样式应用于网页之前,短暂显示网页的无样式版本。
当浏览器遇到没有 defer 或 async 属性的 <script> 元素时,也会阻止网页的解析和渲染。
这是因为,在主 HTML 解析器仍在执行其任务时,浏览器无法确定任何给定脚本是否会修改 DOM。因此,在文档末尾加载 JavaScript 已成为一种常见做法,这样可以最大限度地减少因解析和渲染被阻塞而造成的影响。
这些都是浏览器应该阻止解析和渲染的合理理由。然而,阻止这两个重要步骤中的任何一个都是不可取的,因为这会延迟发现其他重要资源,从而影响节目的正常播放。幸运的是,浏览器会尽力通过一种称为预加载扫描器的辅助 HTML 解析器来缓解这些问题。
<body> 元素中的图片标记,因此会被阻塞,但预加载扫描器可以在原始标记中提前查找该图片资源,并在主 HTML 解析器解除阻塞之前开始加载该资源。
预加载扫描器的角色是推测性的,这意味着它会检查原始标记,以便在主 HTML 解析器发现资源之前,抢先找到要提取的资源。
如何判断预加载扫描器是否正在运行
预加载扫描器之所以存在,是因为渲染和解析被阻塞。如果这两个性能问题从未存在过,预加载扫描器就不会很有用。要确定网页是否能从预加载扫描程序中受益,关键在于了解这些阻塞现象。为此,您可以为请求引入人为延迟,以了解预加载扫描器的工作位置。
以包含样式表的此网页为例,其中包含一些基本文字和图片。由于 CSS 文件会同时阻止渲染和解析,因此您通过代理服务为样式表引入了 2 秒的人为延迟。此延迟可让您更轻松地在网络瀑布图中查看预加载扫描器的工作位置。
如瀑布图所示,即使在渲染和文档解析被阻塞时,预加载扫描程序也会发现 <img> 元素。如果不进行此优化,浏览器就无法在阻塞期间机会性地提取内容,并且更多资源请求会是连续的,而不是并发的。
在了解了上述玩具示例后,我们来看看一些真实世界的模式,在这些模式下,预加载扫描器可能会被绕过,以及可以采取哪些措施来修复这些模式。
注入的 async 脚本
假设您的 <head> 中包含一些内嵌 JavaScript,如下所示:
<script>
const scriptEl = document.createElement('script');
scriptEl.src = '/yall.min.js';
document.head.appendChild(scriptEl);
</script>
注入的脚本默认情况下是 async,因此当注入此脚本时,其行为将如同应用了 async 属性一样。这意味着它会尽快运行,并且不会阻止渲染。听起来很理想,对吧?不过,如果您假设此内嵌 <script> 位于加载外部 CSS 文件的 <link> 元素之后,您会得到次优结果:
async 脚本。预加载扫描程序无法在阻塞渲染阶段发现脚本,因为该脚本是在客户端注入的。
我们来详细了解一下发生了哪些情况:
- 在 0 秒时,系统会请求主文档。
- 在 1.4 秒时,导航请求的第一个字节到达。
- 在 2.0 秒时,系统会请求 CSS 和图片。
- 由于解析器被阻止加载样式表,并且注入
async脚本的内嵌 JavaScript 在 2.6 秒时位于该样式表之后,因此该脚本提供的功能无法尽快使用。
这是次优的,因为只有在样式表下载完成后才会请求脚本。这会延迟脚本的运行时间,使其无法尽快运行。相比之下,由于 <img> 元素在服务器提供的标记中可被发现,因此预加载扫描器会发现该元素。
那么,如果您使用带有 async 属性的常规 <script> 标记,而不是将脚本注入到 DOM 中,会发生什么情况?
<script src="/yall.min.js" async></script>
结果如下:
async <script> 元素。预加载扫描程序在阻塞渲染阶段发现脚本,并与 CSS 并发加载该脚本。
有人可能会建议使用 rel=preload 来解决这些问题。这种方法肯定可行,但可能会产生一些副作用。毕竟,如果通过不将 <script> 元素注入 DOM 即可避免问题,为何还要使用 rel=preload 来解决问题?
async 脚本,但 async 脚本已预加载,以确保它能更快被发现。
预加载“修复”了此问题,但引入了一个新问题:前两个演示中的 async 脚本(尽管已在 <head> 中加载)以“低”优先级加载,而样式表以“最高”优先级加载。在预加载 async 脚本的最后一个演示中,样式表的加载优先级仍然为“最高”,但脚本的优先级已提升为“高”。
当资源的优先级提高时,浏览器会为其分配更多带宽。这意味着,即使样式表的优先级最高,脚本的提升优先级也可能会导致带宽争用。这可能是连接速度较慢或资源非常庞大时的因素。
此处的答案很简单:如果启动期间需要脚本,请勿通过将其注入 DOM 来阻止预加载扫描器。根据需要,尝试调整 <script> 元素放置位置,以及 defer 和 async 等属性。
使用 JavaScript 进行延迟加载
延迟加载是一种节省流量的好方法,通常应用于图片。不过,有时延迟加载会错误地应用于“首屏”中的图片。
这会给预加载扫描器带来潜在的资源可发现性问题,并会不必要地延迟发现对图片的引用、下载图片、解码图片和呈现图片所需的时间。我们以以下图片标记为例:
<img data-src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
使用 data- 前缀是基于 JavaScript 的延迟加载器中的一种常见模式。当图片滚动到视口中时,延迟加载器会剥离 data- 前缀,这意味着在上例中,data-src 会变为 src。此更新会提示浏览器提取资源。
在启动期间,此模式应用于视口中的图片时才会出现问题。由于预加载扫描器读取 data-src 属性的方式与读取 src(或 srcset)属性的方式不同,因此无法更早地发现图片引用。更糟糕的是,图片会延迟加载,直到延迟加载器 JavaScript 下载、编译和执行后才会加载。
根据图片的大小(可能取决于视口的大小),它可能是 Largest Contentful Paint (LCP) 的候选元素。当预加载扫描器无法提前(可能是在网页的样式表阻止渲染时)推测性地提取图片资源时,LCP 会受到影响。
解决方案是更改图片标记:
<img src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
这是启动时位于视口中的图片的最佳模式,因为预加载扫描器会更快地发现并提取图片资源。
在此简化示例中,在慢速连接上,LCP 缩短了 100 毫秒。这看起来可能不算很大的改进,但考虑到该解决方案只是一个快速标记修复,而且大多数网页都比这组示例复杂,因此这确实是一项重大改进。这意味着 LCP 候选元素可能需要与许多其他资源争夺带宽,因此此类优化变得越来越重要。
CSS 背景图片
请注意,浏览器预加载扫描程序会扫描标记。它不会扫描其他资源类型,例如 CSS(可能涉及提取 background-image 属性引用的图片)。
与 HTML 类似,浏览器会将 CSS 处理成自己的对象模型,即 CSSOM。如果在构建 CSSOM 时发现外部资源,则会在发现时请求这些资源,而不是通过预加载扫描器请求。
假设网页的 LCP 候选元素是具有 CSS background-image 属性的元素。以下是资源加载时发生的情况:
background-image 属性的元素(第 3 行)。在 CSS 解析器找到所请求的图片之前,不会开始提取该图片。
在这种情况下,预加载扫描器并非被击败,而是未参与。即便如此,如果网页上的 LCP 候选对象来自 background-image CSS 属性,您仍应预加载该图片:
<!-- Make sure this is in the <head> below any
stylesheets, so as not to block them from loading -->
<link rel="preload" as="image" href="lcp-image.jpg">
虽然 rel=preload 提示很小,但它有助于浏览器比平时更快地发现图片:
background-image 属性的元素(第 3 行)。rel=preload 提示有助于浏览器比没有提示时提前约 250 毫秒发现图片。
借助 rel=preload 提示,系统可以更快地发现 LCP 候选元素,从而缩短 LCP 时间。虽然该提示有助于解决此问题,但更好的选择可能是评估您的图片 LCP 候选对象是否必须从 CSS 加载。借助 <img> 标记,您可以更好地控制加载适合视口的图片,同时允许预加载扫描器发现该图片。
内嵌的资源过多
内嵌是一种将资源放置在 HTML 中的做法。您可以使用 Base64 编码内嵌 <style> 元素中的样式表、<script> 元素中的脚本以及几乎任何其他资源。
内嵌资源可能比下载资源更快,因为无需为资源单独发出请求。它就在文档中,并且会立即加载。不过,这种方法存在明显的缺点:
- 如果您不缓存 HTML(如果 HTML 响应是动态的,您就无法缓存),则内嵌资源永远不会被缓存。这会影响性能,因为内嵌资源无法重复使用。
- 即使您可以缓存 HTML,内嵌资源也不会在文档之间共享。与可在整个来源中缓存和重用的外部文件相比,这会降低缓存效率。
- 如果您内嵌的内容过多,预加载扫描器会延迟发现文档中后面的资源,因为下载额外的内嵌内容需要更长时间。
以此页面为例。在某些情况下,LCP 候选对象是页面顶部的图片,而 CSS 位于由 <link> 元素加载的单独文件中。该网页还使用了四种网络字体,这些字体是从 CSS 资源中作为单独的文件请求的。
<img> 元素加载的图片,但由于网页加载所需的 CSS 和字体位于单独的资源中,因此预加载扫描器会发现该候选对象,而不会延迟预加载扫描器执行其工作。
现在,如果 CSS 和所有字体都内嵌为 base64 资源,会发生什么情况?
<img> 元素加载的图片,但 `` 中 CSS 及其四种字体资源的内嵌会延迟预加载扫描器发现图片的时间,直到这些资源完全下载完毕。
在此示例中,内嵌会给 LCP 带来负面影响,并对整体性能造成负面影响。不内嵌任何内容的网页版本大约在 3.5 秒内绘制 LCP 图片。内嵌所有内容的网页在 7 秒多后才绘制 LCP 图片。
这里涉及的不仅仅是预加载扫描器。内嵌字体并不是一个好策略,因为 base64 是一种低效的二进制资源格式。另一个相关因素是,除非 CSSOM 确定有必要,否则不会下载外部字体资源。当这些字体以内嵌的 base64 形式存在时,无论当前网页是否需要,都会下载这些字体。
预加载能否改进此处的体验?好的。您可以预加载 LCP 图片并缩短 LCP 时间,但使用内嵌资源扩充可能无法缓存的 HTML 会带来其他负面性能后果。首次内容渲染 (FCP) 也会受到此模式的影响。在未内嵌任何内容的网页版本中,FCP 大约为 2.7 秒。在所有内容都内嵌的版本中,FCP 大约为 5.8 秒。
请务必谨慎地将内容内嵌到 HTML 中,尤其是 base64 编码的资源。一般不建议这样做,除非资源非常小。尽可能减少内嵌,因为内嵌过多会带来风险。
使用客户端 JavaScript 呈现标记
毫无疑问,JavaScript 肯定会影响网页速度。开发者不仅依赖它来提供互动性,而且还倾向于依赖它来传递内容本身。这在某些方面可以带来更好的开发者体验;但开发者受益并不总是意味着用户受益。
一种可以绕过预加载扫描器的模式是使用客户端 JavaScript 呈现标记:
当标记载荷完全包含在浏览器中的 JavaScript 中并由其呈现时,该标记中的任何资源对预加载扫描器来说实际上都是不可见的。这会延迟重要资源的发现,从而影响 LCP。在这些示例中,与不需要 JavaScript 即可显示的等效服务器渲染体验相比,LCP 图片的请求被显著延迟。
这有点偏离本文的重点,但渲染客户端标记的效果远不止于击败预加载扫描器。首先,引入 JavaScript 来支持不需要它的体验会增加不必要的处理时间,从而影响 Interaction to Next Paint (INP)。与服务器发送的相同数量的标记相比,在客户端上呈现极大量的标记更有可能生成长时间运行的任务。除了 JavaScript 涉及的额外处理之外,造成这种情况的原因是,浏览器会从服务器流式传输标记,并以倾向于限制长时间运行任务的方式分块渲染。另一方面,客户端呈现的标记会作为单个整体任务来处理,这可能会影响网页的 INP。
此情形的补救措施取决于以下问题的答案:是否有理由说明为什么网页的标记无法由服务器提供,而只能在客户端上呈现?如果答案为“否”,则应尽可能考虑使用服务器端渲染 (SSR) 或静态生成的标记,因为这有助于预加载扫描器提前发现并机会性地提取重要资源。
如果您的网页确实需要 JavaScript 来为网页标记的某些部分附加功能,您仍然可以使用 SSR 来实现此目的,无论是使用原生 JavaScript 还是使用水合,都能兼顾两方面的优势。
帮助预加载扫描器为您提供帮助
预加载扫描器是一种非常有效的浏览器优化功能,可帮助网页在启动期间更快地加载。通过避免会妨碍浏览器提前发现重要资源的模式,您不仅可以简化开发工作,还可以打造更好的用户体验,从而在许多指标(包括一些网页指标)方面取得更好的效果。
总结一下,您应该从本文中了解以下内容:
- 浏览器预加载扫描程序是一个辅助 HTML 解析器,如果主解析器被阻塞,它会提前扫描,以便有机会发现可以更快提取的资源。
- 预加载扫描器无法发现初始导航请求中服务器提供的标记中不存在的资源。绕过预加载扫描器的方法可能包括(但不限于):
- 使用 JavaScript 将资源注入 DOM,无论是脚本、图片、样式表,还是其他最好放在服务器的初始标记载荷中的内容。
- 使用 JavaScript 解决方案延迟加载首屏中的图片或 iframe。
- 在客户端上呈现可能包含对文档子资源的引用的标记(使用 JavaScript)。
- 预加载扫描器仅扫描 HTML。它不会检查其他资源(尤其是 CSS)的内容,这些资源可能包含对重要资源(包括 LCP 候选对象)的引用。
如果出于任何原因,您无法避免使用会负面影响预加载扫描器加速加载性能的模式,请考虑使用 rel=preload 资源提示。如果您确实要使用 rel=preload,请在实验室工具中进行测试,以确保它能带来理想的效果。最后,不要预加载过多的资源,因为如果您将所有内容都设为优先,那么实际上没有任何内容是优先的。
资源
- 通过脚本注入的“异步脚本”被认定为有害
- 浏览器预加载器如何加快网页加载速度
- 预加载关键素材资源以提高加载速度
- 尽早建立网络连接,以提高网页速度的感知速度
- 优化 Largest Contentful Paint
Unsplash 上的主打图片,由 Mohammad Rahmani 拍摄。