优化资源加载

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

网页加载时,其 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,或者默认情况下为 defertype=module)时,解析器会从发现文件到下载、解析和执行文件期间被阻塞。当使用内嵌 JavaScript 时,解析器也会被阻塞,直到内嵌脚本被解析和执行为止。

预加载扫描器

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

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

  • CSS 使用 background-image 属性加载的图片。这些图片引用位于 CSS 中,预加载扫描器无法发现它们。
  • 动态加载的脚本,以 <script> 元素标记的形式注入 到 DOM 中,使用 JavaScript 或使用 动态 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。捆绑器等工具可以在生产版本中自动为您执行此优化。

移除未使用的 CSS

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

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

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

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

避免 CSS @import 声明

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

/* 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 中的符号。此过程有时称为“丑化”。 如需了解其中的区别,请参阅以下 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 提供的功能。

如果您使用捆绑器处理网站的源代码,则通常会自动为生产版本执行丑化。丑化器(例如 Terser)也是高度可配置的,这让您可以调整丑化算法的激进程度,以实现最大的节省。不过,任何丑化工具的默认设置通常足以在输出大小和功能保留之间取得适当的平衡。

JavaScript 演示

知识测验

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

CSS @import 声明。
请重试。
多个 <link> 元素。
正确!

浏览器预加载扫描器有什么用途?

它是一个辅助 HTML 解析器,用于检查原始标记,以便在 DOM 解析器之前发现 资源,从而更早地发现资源。
正确!
检测 HTML 资源中的 <link rel="preload"> 元素。
请重试。

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

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

接下来播放:使用资源提示帮助浏览器

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