在 2018 年 Google IO 大会上,我们介绍了各种工具、库和优化技术,这些工具、库和优化技术可让您更轻松地提升网站性能。本文将以 Oodles Theater 应用为例,介绍这些技术。我们还将介绍在预测性加载方面的实验以及新的 Guess.js 计划。
过去一年,我们一直忙于研究如何让 Web 变得更快、性能更强。这催生了新的工具、方法和库,我们想在本文中与您分享。在第一部分中,我们将展示我们在开发 The Oodles Theater 应用时实际使用的一些优化技巧。在第二部分中,我们将介绍我们在预测性加载方面的实验以及新的 Guess.js 计划。
对效果的需求
互联网每年都在变得越来越重。如果我们查看网络状态,就会发现移动设备上的网页中位数为 1.5MB 左右,其中大部分是 JavaScript 和图片。
网站规模不断扩大,再加上其他因素(例如网络延迟时间、CPU 限制、渲染阻塞模式或多余的第三方代码),导致性能问题变得更加复杂。
大多数用户都将速度视为其需求的用户体验层次结构中的最高优先级。这并不太令人意外,因为在网页完成加载之前,您实际上无法执行太多操作。您无法从网页中获得价值,也无法欣赏其美感。
我们知道,性能对用户来说非常重要,但用户可能不知道从何处开始优化。幸运的是,有一些工具可以帮助您实现这一目标。
Lighthouse - 性能工作流的基础
Lighthouse 是 Chrome 开发者工具的一部分,可用于审核网站,并提供有关如何改进网站的提示。
我们最近推出了一系列新的性能审核,这些审核在日常开发工作流程中非常有用。
让我们通过一个实际示例来了解如何利用它们:Oodles Theater 应用。这是一个小型演示 Web 应用,您可以在其中试用我们最喜欢的一些互动式 Google Doodle,甚至玩一两款游戏。
在构建应用时,我们希望确保其性能尽可能出色。优化的起点是 Lighthouse 报告。
从 Lighthouse 报告中可以看出,我们应用的初始性能非常糟糕。在 3G 网络上,用户需要等待 15 秒才能看到首次有意义的绘制,或等待应用进入互动状态。Lighthouse 突出显示了我们网站的许多问题,而 23 的总体性能得分也恰好反映了这一点。
该网页的大小约为 3.4MB,我们迫切需要减少一些不必要的元素。
这开启了我们的第一个性能挑战:找到可以轻松移除且不会影响整体体验的内容。
效果优化机会
移除不必要的资源
有些显而易见的内容可以安全地移除:空格和注释。
Lighthouse 会在未缩减的 CSS 和 JavaScript 审核中突出显示此机会。我们当时在构建过程中使用 webpack,因此为了实现压缩,我们只需使用 Uglify JS 插件。
缩小化是一项常见任务,因此无论您使用哪种构建流程,都应该能够找到现成的解决方案。
该空间中的另一项实用审核是启用文本压缩。没有理由发送未压缩的文件,而且如今大多数 CDN 都支持开箱即用。
我们使用 Firebase Hosting 来托管代码,而 Firebase 默认启用 gzip 压缩,因此仅凭在合理的 CDN 上托管代码,我们就免费获得了这项功能。
虽然 gzip 是一种非常受欢迎的压缩方式,但 Zopfli 和 Brotli 等其他机制也越来越受欢迎。大多数浏览器都支持 Brotli,您可以使用二进制文件预压缩资源,然后再将其发送到服务器。
使用高效的缓存政策
我们的下一步是确保不会在不必要的情况下发送两次资源。
Lighthouse 中的缓存政策效率低下审核帮助我们注意到,我们可以优化缓存策略,以实现这一目标。通过在服务器中设置 max-age 过期标头,我们确保用户在再次访问时可以重复使用之前下载的资源。
理想情况下,您应尽可能安全地缓存尽可能多的资源,并尽可能延长缓存时间,同时提供验证令牌,以便高效地重新验证已更新的资源。
移除未使用的代码
到目前为止,我们移除了不必要下载的明显部分,但不太明显的部分呢?例如,未使用的代码。
有时,我们会在应用中包含一些并非真正必要的代码。如果您长时间开发应用,团队或依赖项发生变化,有时会遗留下孤立的库,这时尤其需要进行清理。我们就是这样做的。
起初,我们使用 Material Components 库快速制作了应用原型。随着时间的推移,我们转向了更自定义的外观和风格,完全忘记了该库。幸运的是,代码覆盖率检查帮助我们重新发现了软件包中的这个 bug。
您可以在开发者工具中查看代码覆盖率统计信息,包括应用的运行时和加载时。您可以在底部屏幕截图中看到两条红色粗条纹 - 我们有超过 95% 的 CSS 未使用,还有大量 JavaScript 未使用。
Lighthouse 还在未使用的 CSS 规则审核中发现了此问题。结果显示,潜在节省空间超过 400 KB。因此,我们返回到代码,并移除了该库的 JavaScript 和 CSS 部分。
这使我们的 CSS 软件包缩小了 20 倍,对于一个只有两行的提交来说,效果相当不错。
当然,这提高了我们的性能得分,并大大缩短了互动时间。
不过,在发生此类变化后,仅查看指标和得分是不够的。 移除实际代码绝非毫无风险,因此您应始终留意潜在的回归问题。
95% 的代码未被使用,但仍有 5% 的代码在某处被使用。显然,我们的某个组件(涂鸦滑块中的小箭头)仍在沿用该库中的样式。不过,由于它非常小,我们可以手动将这些样式重新纳入按钮中。
因此,如果您要移除代码,请务必确保您已建立适当的测试工作流程,以帮助您防范潜在的视觉回归。
避免过大的网络载荷
我们知道,大型资源会减慢网页加载速度。它们可能会让用户花费金钱,并对用户的数据流量套餐产生很大影响,因此务必要注意这一点。
Lighthouse 能够通过巨大的网络载荷审核检测到我们的一些网络载荷存在问题。
我们发现,当时有超过 3 MB 的代码被向下传递,这在移动设备上尤其显得非常多。
在此列表的最顶部,Lighthouse 突出显示了我们有一个 JavaScript 供应商软件包,其中包含 2 MB 的未压缩代码。这也是 webpack 突出显示的问题。
俗话说得好:最快的请求是不发出的请求。
理想情况下,您应该衡量向用户提供的每项素材资源的价值,衡量这些素材资源的成效,并决定是否值得在初始体验中实际提供这些素材资源。因为有时这些资源可以延迟或延迟加载,或者在空闲时间处理。
在本例中,由于我们处理的是大量 JavaScript 软件包,因此很幸运,因为 JavaScript 社区提供了丰富的 JavaScript 软件包审核工具。
我们首先使用了 webpack bundle analyzer,它告诉我们,我们包含了一个名为 unicode 的依赖项,该依赖项的解析 JavaScript 大小为 1.6 MB,相当大。
然后,我们前往编辑器,使用 Visual Code 的 Import Cost 插件,直观地了解我们导入的每个模块的费用。这样一来,我们就能发现哪个组件包含引用此模块的代码。
然后,我们改用另一个工具 BundlePhobia。借助此工具,您可以输入任何 NPM 软件包的名称,并实际查看其缩小和压缩后的估计大小。我们找到了一个不错的替代方案,用于替换我们之前使用的 slug 模块,该方案仅重 2.2kb,因此我们进行了切换。
这对我们的表现产生了很大影响。通过这项更改以及发现其他可缩减 JavaScript 软件包大小的机会,我们节省了 2.1 MB 的代码。
考虑到这些软件包的 gzip 压缩和最小化大小,我们发现总体改进幅度为 65%。 我们发现,作为一种流程,这样做确实值得。
因此,一般来说,请尽量避免在网站和应用中进行不必要的下载。 清点您的资产并衡量其对效果的影响会带来非常大的变化,因此请确保您定期审核资产。
通过代码拆分缩短 JavaScript 启动时间
虽然大型网络载荷会对我们的应用产生很大影响,但还有另一件事也会产生非常大的影响,那就是 JavaScript。
JavaScript 是您最昂贵的资产。 在移动设备上,如果您发送大型 JavaScript 软件包,可能会延迟用户与界面组件的互动时间。这意味着,他们可能会点按界面,但实际上不会发生任何有意义的事情。因此,我们必须了解 JavaScript 为何会产生如此高的费用。
浏览器就是这样处理 JavaScript 的。
我们首先必须下载该脚本,然后 JavaScript 引擎需要解析该代码、对其进行编译并执行。
现在,在台式机、笔记本电脑甚至高端手机等高端设备上,这些阶段不会花费太多时间。但在中等配置的手机上,此过程可能需要长达 5 到 10 倍的时间。这会延迟互动,因此我们必须尽量缩短此时间。
为了帮助您发现应用中的这些问题,我们为 Lighthouse 引入了新的 JavaScript 启动时间审核。
对于 Oodle 应用,它告诉我们,我们在 JavaScript 启动中花费了 1.8 秒的时间。当时的情况是,我们将所有路由和组件静态导入到一个庞大的 JavaScript 软件包中。
一种解决方法是使用代码拆分。
代码拆分是指,与其向用户提供整个披萨大小的 JavaScript,不如在用户需要时一次只向他们提供一块披萨。
代码拆分可以在路由级或组件级应用。它可与 React 和 React Loadable、Vue.js、Angular、Polymer、Preact 以及其他多个库完美搭配使用。
我们将代码拆分纳入了应用中,从静态导入切换到了动态导入,从而可以根据需要异步延迟加载代码。
这不仅缩小了我们的软件包大小,还缩短了 JavaScript 启动时间。将时间缩短至 0.78 秒,使应用速度提高了 56%。
一般来说,如果您要构建 JavaScript 密集型体验,请务必仅向用户发送他们需要的代码。
您可以利用代码拆分等概念,探索 tree shaking 等想法,并查看 webpack-libs-optimizations 代码库,了解如何缩减库的大小(如果您恰好在使用 webpack)。
优化图像
在 Oodle 应用中,我们使用了大量图片。遗憾的是,Lighthouse 对此的评价远不如我们热衷。事实上,我们未通过所有三项与图片相关的审核。
我们忘记优化图片,图片尺寸也不正确,而且使用其他图片格式也能获得一些增益。
我们首先优化了图片。
对于一次性优化,您可以使用 ImageOptim 或 XNConvert 等可视化工具。
一种更自动化的方法是使用 imagemin 等库向构建流程添加图片优化步骤。
这样,您就可以确保日后添加的图片会自动得到优化。 某些 CDN(例如 Akamai)或第三方解决方案(例如 Cloudinary、Fastly 或 Uploadcare)可为您提供全面的图片优化解决方案,因此您也可以直接在这些服务上托管图片。
如果您不想这样做,可能是因为费用或延迟时间问题,那么 Thumbor 或 Imageflow 等项目可提供自托管替代方案。
我们的背景 PNG 在 webpack 中被标记为大尺寸,这是正确的。在将其尺寸正确调整为视口大小并使用 ImageOptim 进行处理后,我们将其大小降至 100 KB,这是可以接受的。
通过对我们网站上的多张图片重复执行此操作,我们能够显著降低网页的总大小。
为动画内容使用合适的格式
GIF 的费用可能非常高昂。令人惊讶的是,GIF 格式最初并非旨在作为动画平台。因此,改用更合适的视频格式可大幅节省文件大小。
在 Oodle 应用中,我们使用 GIF 作为首页上的介绍序列。根据 Lighthouse 的数据,如果我们改用更高效的视频格式,可以节省超过 7 MB 的空间。我们的剪辑大约为 7.3 MB,对于任何合理的网站来说都太大了,因此我们将其转换为包含两个源文件的视频元素 - 一个 mp4 和一个 WebM,以获得更广泛的浏览器支持。
我们使用 FFmpeg 工具将动画 GIF 转换为 MP4 文件。 WebM 格式可为您节省更多空间 - ImageOptim API 可以为您执行此类转换。
ffmpeg -i animation.gif -b:v 0 -crf 40 -vf scale=600:-1 video.mp4
通过这种转换,我们成功节省了 80% 以上的总体重量。这使我们降至 1 MB 左右。
不过,1 MB 仍是需要通过网络推送的大型资源,尤其是对于带宽受限的用户而言。幸运的是,我们可以使用 Effective Type API 来了解用户使用的是低带宽网络,并向他们提供小得多的 JPEG 图片。
此接口使用有效往返时间和降速值来估计用户正在使用的网络类型。它只是返回一个字符串,即“慢速 2G”“2G”“3G”或“4G”。因此,根据此值,如果用户使用的是 4G 以下的网络,我们可以将视频元素替换为图片。
if (navigator.connection.effectiveType) { ... }
这确实会稍微影响体验,但至少在连接速度较慢的情况下,网站仍可正常使用。
延迟加载屏幕外图片
轮播界面、滑块或很长的网页通常会加载图片,即使这些图片用户无法立即在网页上看到。
Lighthouse 会在“屏幕外图片”审核中标记此行为,您也可以在开发者工具的网络面板中自行查看。如果您发现传入的图片很多,但网页上只显示了少数图片,则可能意味着您可以考虑改为延迟加载这些图片。
浏览器尚不支持原生延迟加载,因此我们必须使用 JavaScript 来添加此功能。我们使用 Lazysizes 库为 Oodle 封面添加了延迟加载行为。
<!-- Import library -->
import lazysizes from 'lazysizes' <!-- or -->
<script src="lazysizes.min.js"></script>
<!-- Use it -->
<img data-src="image.jpg" class="lazyload"/>
<img class="lazyload"
data-sizes="auto"
data-src="image2.jpg"
data-srcset="image1.jpg 300w,
image2.jpg 600w,
image3.jpg 900w"/>
Lazysizes 非常智能,因为它不仅会跟踪元素的可视性变化,还会主动预提取视图附近的元素,以实现最佳用户体验。它还提供 IntersectionObserver 的可选集成,可让您非常高效地查找可见性。
进行此更改后,我们的图片会按需提取。如果您想深入了解该主题,请查看 images.guide,这是一个非常实用且全面的资源。
帮助浏览器尽早提供关键资源
通过网络发送到浏览器的每个字节的重要性各不相同,浏览器也知道这一点。许多浏览器都有启发式方法来决定应首先提取哪些内容。因此,有时它们会在图片或脚本之前提取 CSS。
作为网页的作者,我们可以告知浏览器哪些内容对我们来说确实非常重要,这可能很有用。幸运的是,在过去几年中,浏览器供应商添加了许多功能来帮助我们解决这个问题,例如 link rel=preconnect、preload 或 prefetch 等资源提示。
这些引入到 Web 平台的功能可帮助浏览器在适当的时间提取适当的内容,并且与使用脚本完成的一些基于自定义加载逻辑的方法相比,这些功能效率更高。
我们来看看 Lighthouse 实际上是如何引导我们有效使用其中一些功能的。
Lighthouse 首先建议我们避免多次往返于任何来源,因为这会产生高昂的费用。
对于 Oodle 应用,我们实际上大量使用了 Google Fonts。每当您将 Google 字体样式表放入网页中时,它都会连接到最多两个子网域。Lighthouse 告诉我们,如果我们能够预热该连接,则可以将初始连接时间缩短最多 300 毫秒。
利用链接 rel preconnect,我们可以有效地掩盖连接延迟。
尤其是对于 Google Fonts 这样的服务,其字体样式 CSS 托管在 googleapis.com 上,字体资源托管在 Gstatic 上,这可能会产生非常大的影响。因此,我们应用了此优化,并节省了数百毫秒的时间。
Lighthouse 建议的下一件事是预加载关键请求。
<link rel=preload> 非常强大,它会告知浏览器当前导航需要某个资源,并尝试让浏览器尽快获取该资源。
现在,Lighthouse 告诉我们,我们应该预加载关键的 Web 字体资源,因为我们正在加载两种 Web 字体。
网页字体中的预加载如下所示:指定 rel=preload,传入 as 并指定字体类型,然后指定要加载的字体类型,例如 woff2。
这会对您的网页产生相当明显的影响。
通常,如果不使用链接 rel 预加载,如果网页字体对您的网页至关重要,浏览器必须先提取 HTML,然后解析 CSS,最后在稍晚的时候提取网页字体。
使用链接 rel 预加载后,浏览器在解析 HTML 后即可立即开始提取这些网络字体。就我们的应用而言,这能够将使用 Web 字体渲染文本所需的时间缩短 1 秒。
不过,如果您打算使用 Google Fonts 预加载字体,情况就没那么简单了,需要注意一个陷阱。
我们在样式表中的字体面上指定的 Google 字体网址恰好是字体团队经常更新的内容。这些网址可能会过期或定期更新,因此,如果您想完全掌控字体加载体验,我们建议您自行托管 Web 字体。这非常有用,因为您可以访问链接 rel 预加载等功能。
就我们而言,我们发现 Google Web Fonts Helper 这款工具非常实用,可帮助我们离线使用部分 Web 字体并将其设置为本地字体,因此不妨试试这款工具。
无论您是使用网络字体作为关键资源的一部分,还是使用 JavaScript,都应尽量帮助浏览器尽快交付关键资源。
实验性功能:优先提示
今天,我们想与您分享一些特别的内容。除了资源提示和预加载等功能之外,我们还一直在开发一项全新的实验性浏览器功能,我们称之为“优先级提示”。
这是一项新功能,可让您向浏览器提示资源的重要性。它公开了一个新属性 - importance,其值可以是 low、high 或 auto。
这样一来,我们就可以传达降低不太重要资源的优先级(例如非关键样式、图片或提取 API 调用)以减少争用的信息。我们还可以提高更重要内容的优先级,例如主打图片。
就我们的 Oodle 应用而言,这实际上为我们提供了一个可以进行优化的实用位置。
在为图片添加延迟加载功能之前,浏览器会执行以下操作:我们有一个包含所有 Doodles 的图片轮播,浏览器会在轮播开始时以高优先级提取所有图片。遗憾的是,对用户而言最重要的图片恰恰是轮播界面中间的图片。因此,我们所做的就是将这些背景图片的优先级设置为非常低,将前景图片的优先级设置为非常高,这样一来,在缓慢的 3G 网络下,我们获取和渲染这些图片的速度快了 2 秒。因此,这是一次非常积极的体验。
我们希望在几周内将此功能引入 Canary,敬请期待。
制定网络字体加载策略
排版对于优秀的设计至关重要,如果您使用的是 Web 字体,理想情况下,您不希望阻止文本的渲染,并且绝对不希望显示不可见的文本。
我们现在通过避免在加载网络字体时显示不可见文本审核在 Lighthouse 中突出显示此问题。
如果您使用字体面块加载网页字体,则表示您允许浏览器自行决定在网页字体提取时间过长时该怎么做。有些浏览器会等待最多三秒钟,然后回退到系统字体,并最终在字体下载完成后将其替换为该字体。
我们正努力避免出现这种不可见的文字,因此在这种情况下,如果网络字体加载时间过长,我们就无法看到本周的经典涂鸦。幸运的是,借助一项名为 font-display 的新功能,您可以更好地控制此流程。
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-display: swap;
font-weight: 400;
src: local('Montserrat Regular'), local('Montserrat-Regular'),
/* Chrome 26+, Opera 23+, Firefox 39+ */
url('montserrat-v12-latin-regular.woff2') format('woff2'),
/* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
url('montserrat-v12-latin-regular.woff') format('woff');
}
字体显示有助于您根据网络字体替换所需的时间来决定其呈现方式或回退方式。
在本例中,我们使用的是字体显示交换。Swap 会为字体指定 0 秒的阻塞周期和无限的交换周期。这意味着,如果字体需要一段时间才能加载,浏览器会立即使用后备字体绘制文本。并且会在字体可用时进行替换。
就我们的应用而言,这种做法之所以很棒,是因为它让我们能够在很早的时候就显示一些有意义的文本,并在网络字体准备就绪后过渡到该字体。
一般来说,如果您恰好在使用网页字体(就像很大一部分网站那样),请制定良好的网页字体加载策略。
您可以使用许多 Web 平台功能来优化字体加载体验,但也要查看 Zach Leatherman 的 Web Font Recipes 代码库,因为该代码库非常出色。
减少阻塞渲染的脚本
我们可以将应用的其他部分提前推送到下载链中,以便更早地提供至少一些基本的用户体验。
在 Lighthouse 时间轴条带上,您可以看到,在加载所有资源的前几秒内,用户实际上看不到任何内容。
下载和处理外部样式表会阻止渲染进程取得任何进展。
我们可以尝试提前提供一些样式,以优化关键渲染路径。
如果我们提取负责初始渲染的样式并将其内嵌到 HTML 中,浏览器便能够立即渲染这些样式,而无需等待外部样式表到达。
在本例中,我们使用了一个名为 Critical 的 NPM 模块,以便在构建步骤中将关键内容内嵌到 index.html 中。
虽然此模块为我们完成了大部分繁重的工作,但要让它在不同路线中顺畅运行,仍然有点棘手。
如果您不小心,或者您的网站结构非常复杂,那么如果您一开始没有规划应用 shell 架构,则可能很难引入这种类型的模式。
因此,尽早考虑性能问题非常重要。如果您一开始没有针对性能进行设计,那么日后很可能会遇到相关问题。
最终,我们的冒险获得了回报,我们成功实现了这一目标,应用开始更早地提供内容,从而显著缩短了首次有效渲染时间。
结果
以上是我们应用于网站的一长串性能优化措施。我们来看看结果。下图显示了我们的应用在 3G 网络上加载到中等移动设备上的情况,左侧是优化前,右侧是优化后。
Lighthouse 性能得分从 23 提高到了 91。就速度而言,这已经取得了相当不错的进展。所有这些更改都是在持续检查和遵循 Lighthouse 报告的指导下完成的。如果您想了解我们如何在技术上实现所有改进,欢迎查看我们的代码库,尤其是其中已合并的 PR。
预测性效果 - 数据驱动型用户体验
我们认为,机器学习在许多领域都代表着令人兴奋的未来机遇。 我们希望未来能激发更多实验的一个想法是,真实数据确实可以指导我们打造的用户体验。
如今,我们会在很大程度上随意决定用户可能想要或需要什么,以及哪些内容值得预提取、预加载或预缓存。如果我们猜对了,就可以优先处理少量资源,但很难将其扩展到整个网站。
实际上,我们现在有数据可供参考,以便更好地进行优化。 借助 Google Analytics Reporting API,我们可以查看网站上任何网址的下一个热门网页和退出百分比,从而得出结论,确定应优先考虑哪些资源。
如果我们将此方法与良好的概率模型相结合,就可以避免因过度预提取内容而浪费用户的数据。我们可以利用这些 Google Analytics 数据,并使用机器学习和模型(例如 Markov 链或神经网络)来实现此类模型。
为了促进这些实验,我们很高兴地宣布推出一项名为 Guess.js 的新计划。
Guess.js 是一个专注于为 Web 提供数据驱动型用户体验的项目。我们希望它能激发您探索如何使用数据来提升网站性能,并在此基础上取得更多成就。所有这些功能都是开源的,并且今天已在 GitHub 上提供。该项目由 Minko Gechev、Gatsby 的 Kyle Matthews、Katie Hempenius 和许多其他人与开源社区合作构建。
欢迎查看 Guess.js,并与我们分享您的想法。
摘要
得分和指标有助于提高网站速度,但它们只是手段,而不是目标本身。
我们都曾有过在旅途中遇到网页加载缓慢的情况,但现在,我们可以为用户提供加载速度非常快的出色体验。
提升效果是一个循序渐进的过程。许多小改变可以带来大收益。通过使用合适的优化工具并密切关注 Lighthouse 报告,您可以为用户提供更优质、更具包容性的体验。
特别鸣谢:Ward Peeters、Minko Gechev、Kyle Mathews、Katie Hempenius、Dom Farolino、Yoav Weiss、Susie Lu、Yusuke Utsunomiya、Tom Ankers、Lighthouse 和 Google Doodles。