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