JavaScript 框架中的资源内嵌

改进整个 JavaScript 生态系统中的 Largest Contentful Paint。

Aurora 项目中,Google 一直在与热门 Web 框架合作,确保它们能根据 Core Web Vitals 指标提供出色的性能。Angular 和 Next.js 已经推出了内嵌字体,本文第一部分对此进行了介绍。我们将介绍的第二项优化是关键 CSS 内嵌,它现在在 Angular CLI 中默认处于启用状态,并且正在 Nuxt.js 中实现。

内嵌字体

在分析了数百个应用后,Aurora 团队发现,开发者通常会在 index.html<head> 元素中引用字体,以便在其应用中添加字体。下面的示例展示了添加 Material Icons 后的效果:

<!doctype html>
<html lang="en">
<head>
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
  ...
</html>

尽管此模式完全有效且正常运行,但它会阻止应用呈现并引入额外的请求。为了更好地了解发生了什么情况,请查看上面 HTML 中引用的样式的源代码:

/* fallback */
@font-face {
  font-family: 'Material Icons';
  font-style: normal;
  font-weight: 400;
  src: url(https://fonts.gstatic.com/font.woff2) format('woff2');
}

.material-icons {
  /*...*/
}

请注意 font-face 定义如何引用 fonts.gstatic.com 上托管的外部文件。加载应用时,浏览器首先必须下载 head 中引用的原始样式表。

一张图片,显示了网站如何向服务器发出请求并下载外部样式表
首先,网站会加载字体样式表。

接下来,浏览器下载 woff2 文件,最后才能够继续渲染应用。

一张图片,显示了发出的两个请求,一个请求用于获取字体样式表,另一个请求用于获取字体文件。
接下来,系统会发出加载字体的请求。

一个优化机会是在构建时下载初始样式表并将其内嵌到 index.html 中。这样一来,系统便会在运行时跳过对 CDN 的整个往返,从而缩短屏蔽时间。

构建应用时,系统会向 CDN 发送请求,这会提取样式表并将其内嵌到 HTML 文件中,并向网域添加 <link rel=preconnect>。应用此技术后,我们会得到以下结果:

<!doctype html>
<html lang="en">
<head>
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin >
  <style type="text/css">
  @font-face{font-family:'Material Icons';font-style:normal;font-weight:400;src:url(https://fonts.gstatic.com/font.woff2) format('woff2');}.material-icons{/*...*/}</style>
  ...
</html>

现在,Next.js 和 Angular 支持内嵌字体

当框架开发者在底层工具中实现优化时,现有应用和新应用便能更轻松地启用优化,从而为整个生态系统带来改进。

从 Next.js v10.2 和 Angular v11 开始,此改进默认处于启用状态。这两种格式都支持内嵌 Google 和 Adobe 字体。Angular 预计会在 v12.2 中引入后者。

您可以在 GitHub 上找到 Next.js 中的内嵌字体实现,并观看介绍在 Angular 环境中进行此优化的视频

内嵌关键 CSS

另一项增强功能涉及通过内嵌关键 CSS 来改进 First Contentful Paint (FCP)Largest Contentful Paint (LCP) 指标。网页的关键 CSS 包括其在首次渲染时使用的所有样式。如需详细了解此主题,请参阅推迟非关键 CSS

我们发现,许多应用会同步加载样式,这会阻止应用呈现。快速解决方法是异步加载样式。请勿使用 media="all" 加载脚本,而是将 media 属性的值设置为 print,并在加载完成后将属性值替换为 all

<link rel="stylesheet" href="..." media="print" onload="this.media='all'">

不过,这种做法会导致没有样式的内容闪烁。

样式加载时,页面似乎在闪烁。

上方视频展示了网页的呈现过程,该网页会异步加载其样式。之所以发生闪烁,是因为浏览器先开始下载样式,然后呈现随后的 HTML。浏览器下载样式后,会触发链接元素的 onload 事件,将 media 属性更新为 all,并将样式应用于 DOM。

在呈现 HTML 和应用样式期间,系统会对网页的一部分取消样式。 当浏览器使用这些样式时,我们会看到闪烁,这会导致用户体验不佳,并导致累计布局偏移 (CLS) 出现回归问题。

关键 CSS 内嵌以及异步样式加载可以改进加载行为。critters 工具会查看样式表中的选择器,并将其与 HTML 进行匹配,以便找出网页上使用的样式。当它找到匹配项时,会将相应的样式视为关键 CSS 的一部分,并将其内嵌。

让我们看看以下示例:

错误做法
<head>
   <link rel="stylesheet" href="/styles.css" media="print" onload="this.media='all'">
</head>
<body>
  <section>
    <button class="primary"></button>
  </section>
</body>
/* styles.css */
section button.primary {
  /* ... */
}
.list {
  /* ... */
}

内嵌之前的示例。

在上面的示例中,critter 会读取并解析 styles.css 的内容,然后将两个选择器与 HTML 进行匹配,并发现我们使用了 section button.primary。最后,Critters 会在网页的 <head> 中内嵌相应的样式,从而产生以下结果:

正确做法
<head>
  <link rel="stylesheet" href="/styles.css" media="print" onload="this.media='all'">
  <style>
  section button.primary {
    /* ... */
  }
  </style>
</head>
<body>
  <section>
    <button class="primary"></button>
  </section>
</body>

内嵌后的示例。

在 HTML 中内嵌关键 CSS 后,您会发现页面不再闪烁:

CSS 内嵌后页面加载。

关键 CSS 内嵌功能现已在 Angular 中推出,并在 v12 中默认处于启用状态。如果您使用的是 v11,可通过在 angular.jsoninlineCritical 属性设置为 true 来启用 v11。如需在 Next.js 中选择启用这项功能,请将 experimental: { optimizeCss: true } 添加到您的 next.config.js 中。

总结

在本博文中,我们介绍了 Chrome 与 Web 框架之间的一些协作。如果您是框架作者,并且发现我们在您的技术中解决了一些问题,我们希望我们的研究成果能激励您应用类似的性能优化。

详细了解改进内容。您可以在Aurora 简介一文中找到我们为 Core Web Vitals 开展的优化工作的完整列表。