优化资源加载

在上一模块中,我们探讨了关键渲染路径背后的一些理论,以及渲染阻塞资源和解析器阻塞资源如何延迟网页的初始渲染。现在,您已经了解了这方面的一些理论知识,接下来可以学习一些优化关键渲染路径的技巧了。

网页加载时,其 HTML 中会引用许多资源,这些资源通过 CSS 为网页提供外观和布局,并通过 JavaScript 提供互动性。本模块将介绍与这些资源相关的一些重要概念,以及它们如何影响网页的加载时间。

渲染阻塞

正如上一个模块中所讨论的,CSS 是一种会阻止渲染的资源,因为它会阻止浏览器渲染任何内容,直到构建 CSS 对象模型 (CSSOM) 为止。浏览器会阻止渲染,以防止出现无样式内容闪烁 (FOUC),这从用户体验的角度来看是不理想的。

在上面的视频中,有一个短暂的 FOUC,您可以看到没有任何样式的网页。随后,当网页的 CSS 完成从网络加载后,所有样式都会应用,并且未设置样式的网页版本会立即替换为设置了样式的版本。

一般来说,您通常不会看到 FOUC,但了解这个概念非常重要,这样您才能知道浏览器为何在下载 CSS 并将其应用于网页之前阻止网页的呈现。渲染阻塞不一定是不良的,但您应通过优化 CSS 来尽可能缩短其持续时间。

解析器阻塞

会阻塞解析器的资源会中断 HTML 解析器,例如不含 asyncdefer 属性的 <script> 元素。当解析器遇到 <script> 元素时,浏览器需要先评估并执行脚本,然后再继续解析其余的 HTML。这是有意为之,因为脚本可能会在 DOM 仍在构建期间修改或访问 DOM。

<!-- This is a parser-blocking script: -->
<script src="/script.js"></script>

使用外部 JavaScript 文件(不含 asyncdefer)时,解析器会从发现文件时起一直被阻塞,直到文件下载、解析和执行完毕。使用内嵌 JavaScript 时,解析器也会被阻塞,直到内嵌脚本被解析和执行。

预加载扫描器

预加载扫描器是一种浏览器优化功能,以辅助 HTML 解析器的形式存在,用于扫描原始 HTML 响应,以在主 HTML 解析器发现资源之前找到并推测性地提取资源。例如,即使 HTML 解析器在提取和处理 CSS 和 JavaScript 等资源时被阻塞,预加载扫描程序也会允许浏览器开始下载 <img> 元素中指定的资源。

为了充分利用预加载扫描器,服务器发送的 HTML 标记中应包含关键资源。预加载扫描器无法发现以下资源加载模式:

  • 通过 CSS 使用 background-image 属性加载的图片。这些图片引用位于 CSS 中,无法被预加载扫描器发现。
  • <script> 元素标记形式注入到 DOM 中的动态加载脚本(使用 JavaScript 或通过 dynamic import() 加载的模块)。
  • 使用 JavaScript 在客户端上呈现的 HTML。此类标记包含在 JavaScript 资源中的字符串内,无法被预加载扫描器发现。
  • CSS @import 声明。

这些资源加载模式都是后期发现的资源,因此无法从预加载扫描器中受益。请尽可能避免。不过,如果无法避免此类模式,您或许可以使用 preload 提示来避免资源发现延迟。

CSS

CSS 可决定网页的呈现方式和布局。如前所述,CSS 是一种会阻塞渲染的资源,因此优化 CSS 可能会对整体网页加载时间产生相当大的影响。

缩小

缩减 CSS 文件大小可减小 CSS 资源的体积,从而加快下载速度。这主要是通过从源 CSS 文件中移除空格和其他不可见字符等内容,并将结果输出到新优化的文件中来实现的:

/* Unminified CSS: */

/* Heading 1 */
h1 {
  font-size: 2em;
  color: #000000;
}

/* Heading 2 */
h2 {
  font-size: 1.5em;
  color: #000000;
}
/* Minified CSS: */
h1,h2{color:#000}h1{font-size:2em}h2{font-size:1.5em}

从最基本的层面来说,CSS 压缩是一种有效的优化方式,可以缩短网站的 FCP,在某些情况下甚至可以缩短 LCP。打包器等工具可以在生产 build 中自动为您执行此优化。

移除未使用的 CSS

在渲染任何内容之前,浏览器需要下载并解析所有样式表。完成解析所需的时间还包括当前网页上未使用的样式。如果您使用的打包器会将所有 CSS 资源合并到一个文件中,那么您的用户下载的 CSS 可能比渲染当前页面所需的 CSS 多。

如需发现当前网页中未使用的 CSS,请使用 Chrome 开发者工具中的覆盖率工具

Chrome 开发者工具中的“覆盖率”工具的屏幕截图。在底部窗格中选择了一个 CSS 文件,其中显示了当前网页布局未使用的相当多的 CSS。
Chrome 开发者工具中的覆盖率工具可用于检测当前网页未使用的 CSS(和 JavaScript)。它可以用于将 CSS 文件拆分为多个资源,以便由不同的网页加载,而不是提供一个可能会延迟网页渲染的更大的 CSS 软件包。

移除未使用的 CSS 有双重效果:除了缩短下载时间之外,您还可以优化渲染树的构建,因为浏览器需要处理的 CSS 规则更少。

避免使用 CSS @import 声明

虽然 @import 声明看起来很方便,但您应避免在 CSS 中使用它:

/* Don't do this: */
@import url('style.css');

与 HTML 中的 <link> 元素类似,CSS 中的 @import 声明可让您从样式表中导入外部 CSS 资源。这两种方法的主要区别在于,HTML <link> 元素是 HTML 响应的一部分,因此比通过 @import 声明下载的 CSS 文件更早被发现。

这是因为,为了发现 @import 声明,必须下载包含该声明的 CSS 文件。这会导致所谓的“请求链”,在 CSS 的情况下,这会延迟网页的初始渲染时间。另一个缺点是,使用 @import 声明加载的样式表无法被预加载扫描器发现,因此会成为后期发现的阻塞渲染的资源。

<!-- Do this instead: -->
<link rel="stylesheet" href="style.css">

在大多数情况下,您可以使用 <link rel="stylesheet"> 元素替换 @import<link> 元素允许并发下载样式表,从而缩短总体加载时间,而 @import 声明则会连续下载样式表。

内嵌关键 CSS

下载 CSS 文件所需的时间可能会增加网页的 FCP。在文档中内嵌关键样式 <head> 可消除对 CSS 资源的网络请求,并且在用户浏览器缓存未预先填充的情况下,如果操作正确,可以缩短初始加载时间。其余 CSS 可以异步加载,也可以附加到 <body> 元素的末尾。

<head>
  <title>Page Title</title>
  <!-- ... -->
  <style>h1,h2{color:#000}h1{font-size:2em}h2{font-size:1.5em}</style>
</head>
<body>
  <!-- Other page markup... -->
  <link rel="stylesheet" href="non-critical.css">
</body>

不过,内嵌大量 CSS 会增加初始 HTML 响应的字节数。由于 HTML 资源通常无法长时间缓存(甚至根本无法缓存),这意味着内嵌 CSS 不会缓存到后续网页中,而这些网页可能会在外部样式表中使用相同的 CSS。测试并衡量网页的性能,确保这些权衡取舍值得付出努力。

CSS 演示

JavaScript

JavaScript 可实现网络上的大部分互动,但会产生一定的费用。 如果 JavaScript 代码过多,网页在加载期间的响应速度会变慢,甚至可能导致响应速度变慢,从而影响互动,这两种情况都会让用户感到沮丧。

会阻止渲染的 JavaScript

加载不含 deferasync 属性的 <script> 元素时,浏览器会阻止解析和渲染,直到脚本下载、解析和执行完毕。同样,内嵌脚本会阻塞解析器,直到脚本被解析和执行。

asyncdefer

asyncdefer 允许加载外部脚本,而不会阻塞 HTML 解析器,同时会自动延迟带有 type="module" 的脚本(包括内嵌脚本)。不过,asyncdefer 之间存在一些重要的区别,您需要了解这些区别。

一张图,描绘了各种脚本加载机制,详细说明了基于各种属性(例如 async、defer、type=&#39;module&#39; 以及这三者的组合)的解析器、提取和执行角色。
资料来源:https://html.spec.whatwg.org/multipage/scripting.html

使用 async 加载的脚本在下载完成后会立即解析并执行,而使用 defer 加载的脚本会在 HTML 文档解析完成后执行,这与浏览器的 DOMContentLoaded 事件同时发生。 此外,async 脚本可能会无序执行,而 defer 脚本则会按照其在标记中出现的顺序执行。

客户端渲染

一般来说,您应避免使用 JavaScript 来呈现任何关键内容或网页的 LCP 元素。这称为客户端呈现,是一种广泛用于单页应用 (SPA) 的技术。

由 JavaScript 呈现的标记会绕过预加载扫描器,因为客户端呈现的标记中包含的资源无法被该扫描器发现。这可能会延迟关键资源(例如 LCP 图片)的下载。浏览器仅在脚本执行完毕并将元素添加到 DOM 后才开始下载 LCP 图片。反过来,脚本只有在被发现、下载和解析后才能执行。这称为“关键请求链”,应避免出现这种情况。

此外,与从服务器下载的标记(以响应导航请求)相比,使用 JavaScript 呈现标记更有可能生成长任务。大量使用 HTML 客户端渲染可能会对互动延迟时间产生不利影响。如果网页的 DOM 非常大,JavaScript 修改 DOM 时会触发大量的渲染工作,在这种情况下尤其如此。

缩小

与 CSS 类似,缩减 JavaScript 可减小脚本资源的文件大小。 这有助于加快下载速度,使浏览器能够更快地开始解析和编译 JavaScript。

此外,JavaScript 的缩减比其他资源(例如 CSS)的缩减更进一步。JavaScript 经过精简后,不仅会去除空格、制表符和注释等内容,还会缩短源 JavaScript 中的符号。此过程有时称为“丑化”(uglification)。uglification为了说明区别,我们来看一下以下 JavaScript 源代码:

// Unuglified JavaScript source code:
export function injectScript () {
  const scriptElement = document.createElement('script');
  scriptElement.src = '/js/scripts.js';
  scriptElement.type = 'module';

  document.body.appendChild(scriptElement);
}

对上述 JavaScript 源代码进行精简后,结果可能类似于以下代码段:

// Uglified JavaScript production code:
export function injectScript(){const t=document.createElement("script");t.src="/js/scripts.js",t.type="module",document.body.appendChild(t)}

在上述代码段中,您可以看到来源中易于理解的变量 scriptElement 缩短为 t。如果应用于大量脚本,节省的费用会相当可观,同时不会影响网站的生产 JavaScript 提供的功能。

如果您使用打包器来处理网站的源代码,通常会自动对正式版 build 进行精简。Uglifier(例如 Terser)也具有高度可配置性,可让您调整丑化算法的激进程度,以实现最大节省。不过,任何代码精简工具的默认设置通常都足以在输出大小和功能保留之间取得适当的平衡。

JavaScript 演示

知识测验

在浏览器中加载多个 CSS 文件的最佳方式是什么?

CSS @import 声明。
多个 <link> 元素。

浏览器预加载扫描程序有何作用?

检测 HTML 资源中的 <link rel="preload"> 元素。
它是一个辅助 HTML 解析器,用于检查原始标记以发现资源,以便比 DOM 解析器更早地发现资源。

为什么浏览器在下载 JavaScript 资源时默认会暂时阻止 HTML 解析?

由于评估 JavaScript 是一项非常消耗 CPU 的任务,暂停 HTML 解析可为 CPU 提供更多带宽来完成脚本加载。
因为脚本可以修改或以其他方式访问 DOM。
防止出现无样式内容闪烁 (FOUC)。

后续内容:使用资源提示协助浏览器

现在,您已经了解了 <head> 元素中加载的资源如何影响初始页面加载和各种指标,接下来可以继续学习了。在下一个模块中,我们将探讨资源提示,以及它们如何为浏览器提供有价值的提示,以便浏览器能够比没有这些提示时更早地开始加载资源并打开与跨源服务器的连接。