轻松实现 Web 性能优化

avatar
花呗借呗前端团队 @蚂蚁集团

原文作者:Addy Osmani

译者:UC 国际研发 Jothy

写在最前:欢迎你来到“UC国际技术”公众号,我们将为大家提供与客户端、服务端、算法、测试、数据、前端等相关的高质量技术文章,不限于原创与翻译。

这是一篇关于性能优化的文章,是一篇非常值得你阅读的文章,文章的内容非常丰富,大概你花5-10分钟阅读。

在过去的一年里,我们忙于试图弄清楚如何让网络更快、更高效。因此就有了我们希望在本文中与你分享的新工具,方法和库。在第一部分中,我们将向你展示我们在开发 The Oodles Theater App 时使用的一些优化技术。在第二部分中,我们将讨论我们的预测加载实验和新的 Guess.js 计划。

Tip:你可以在 Youtube 观看视频 https://www.youtube.com/watch?v=Mv-l3-tJgGk


性能的需要

互联网每年都在变得越来越重。如果我们检查网页的状态,我们可以看到一个中等大小的移动端页面大约为 1.5MB,其中大部分是 JavaScript 和图片。

网站规模不断扩大,加上其他因素,如网络延迟,CPU 限制,渲染阻塞模式或多余的第三方代码,都会导致复杂的性能难题。

大多数用户把速度当成用户体验需要层次理论(User Experience Hierarchy of Needs,见下图)的最高层。这并不太令人惊讶,因为在页面加载完成之前,你有很多事情都做不了。你无法从页面中获取价值,你无法欣赏它的美学。


图1. 速度对用户有多重要?

我们知道性能对用户很重要,但它又像是一个寻找从哪里开始优化的秘密。幸好有一些工具可以帮助我们。



Lighthouse ——性能工作流的基础

Lighthouse 属于 Chrome DevTools 的一部分,它允许你对你的网站进行审查,并提供优化建议。

我们最近推出了一系列新的性能审查(https://developers.google.com/web/updates/2018/05/lighthouse),它们在日常开发工作流中非常有用。

图2. 新 Lighthouse 审查


让我们来探索一下如何在一个实际的例子中利用它们:Oodles Theatre App。这是一个小型的 demo web app,你可以在里面试用我们最爱的一些交互式 Google Doodle,甚至玩一两局游戏。

在构建 App 时,我们希望确保它尽可能高效。优化的起点是 Lighthouse 报告。

图3. Oodles App 的 Lighthouse 报告

我们的 App 在 Lighthouse 报告中的初始表现非常糟糕。使用 3G 网络时,用户需要等待 15 秒才能获得第一个有意义的(页面)绘制,或者(说)让 App 可交互。 Lighthouse 高亮标识了我们网站的一堆问题,而 23 分的整体性能得分充分反映了这一点。

页面大小约3.4MB - 我们迫切需要减减肥了。

我们的首次性能挑战由此开始:找到可以轻松删除的内容,同时不影响整体体验。


性能优化机会

1. 删除不必要的资源

有一些显而易见的东西可以安全删除掉:空白和注释。

图4. Minify和压缩 JavaScript 和 CSS

Lighthouse 在 Unminified CSS & JavaScript 审查中突出了这个机会。程序使用 webpack 进行构建,因此为了缩小体积,我们选择了 Uglify JS 插件。

缩小体积是一项常见任务,因此你得找到适合你的构建过程的现成解决方案。

该项目中另一个有用的审查是启用文本压缩。我们没有理由发送未压缩的文件,而且现在大多数 CDN 都开箱即用地支持这个。

我们使用 Firebase Hosting 来托管我们的代码,Firebase 默认启用 gzip,因此,通过在合理的 CDN 上托管我们的代码,我们免费获得了这个功能。

虽然 gzip 是一种非常流行的压缩方式,但其他机制如 Zopfli 和 Brotli 也越来越吸引人。 Brotli 受大多数浏览器支持,你可以在将资源发送到服务器之前使用二进制方式对其进行预压缩。



2. 使用有效的缓存策略

我们的下一步是确保在不必要的情况下我们不会重复发送资源。

Lighthouse 中的低效缓存策略审查使我们注意到,我们可以通过优化缓存策略实现这一目标。通过在我们的服务器中设置 max-age 过期头,我们可以确保在重复访问情况下,用户可以重用他们之前下载的资源。

理想情况下,你应该在尽可能长的时间内尽可能安全地缓存尽可能多的资源,并提供验证令牌,以便高效地重新验证已更新的资源。



3. 删除未使用的代码

到目前为止,我们删除了非必要下载文件的显要部分,那不太明显的部分呢?例如,未使用的代码。

图5.检查代码覆盖率

有时我们会在 App 中包含不必要的代码。这种情况尤其是在你的 App 开发了很长一段时间,你的团队或你的依赖项发生了变化,有时“孤儿库”被遗忘的情况下。这正是我们经历过的事情。

最初我们使用 Material Components 库来快速构建我们的 App 原型。随着时间的推移,我们转向了更加定制化的外观,我们完全忘记了这个库。幸运的是,代码覆盖率检查帮助我们在 bundle 中重新发现了它。

你可以在 DevTools 中检查代码覆盖率状态,包括运行时和应用加载时间。你可以在底部截图中看到两个大的红色横条 - 我们有超过 95% 的 CSS 是未使用的,还有一大堆 JavaScript。

Lighthouse 在未使用的 CSS 规则审查中也提到了这个问题。它表示我们可能节省超过 400kb。所以我们回到代码里,删除了该库的 JavaScript 和 CSS 部分。

图6. 弃用 MVC 适配器,我们的样式下降到了 10KB!


这使我们的 CSS bundle 减少了 20倍,这对于一个很小的,两行长的 commit 来说太赞了。

当然,它使我们的性能得分上升,而且可交互时间也得到优化。

但是,通过这样的更改,仅仅检查指标和分数是不够的。删除实际代码绝不是零风险的,因此你应该始终注意发掘潜在的风险。

我们的代码在 95% 的代码中未被使用 - 在某处仍有 5% 在使用。显然我们的某个组件仍在使用该库中的样式 - 涂鸦滑块中的小箭头。因为它太小了,我们可以手动将这些样式合并到按钮中。

图7.一个组件仍在使用已删除的库

因此,如果您删除代码,请确保您拥有适当的测试工作流,以帮助您防范潜在的可见的风险回归。



4. 避免巨大的网络负载

我们知道大量资源可能会减慢网页加载速度。他们可能会消耗用户的钱,他们可能会对他们的数据计划产生重大影响,因此注意这一点非常重要。

通过使用巨大的网络负载审查,Lighthouse 能够检测到我们的某些网络负载存在的问题。

图8.检测巨大的网络负载

从这里可以看到,我们有超过 3mb 的代码被传输 - 这不是一般的多,特别是在移动设备上。

在这个列表的最顶部,Lighthouse 告诉我们有一个2mb大小的未压缩JavaScript vendor 包。这也是 webpack 高亮显示的问题。

俗话说:最快的请求是还没发出的请求。

理想情况下,你应该衡量你发送给用户的每一项资源的价值,衡量这些资源的性能,并根据初始经验判断它是否真的值得被传输。因为有时这些资源可以在空闲时间延迟发送,懒加载或处理。

在我们的例子中,因为我们处理的是大量的 JavaScript 包,所以我们很幸运,因为 JavaScript 社区拥有丰富的 JavaScript 包审查工具。

图9. JavaScript 包审查

我们开始使用 webpack bundle analyzer,它告诉我们,我们有一个名为 unicode 的依赖项,它是 1.6mb 的已解析 JavaScript,非常大的文件。

然后我们转到我们的编辑器,并使用为可视化代码准备的 Import Cost 插件,透过它我们能够看到我们导入的每个模块的成本。它能帮助我们发现哪个组件包含引用此模块的代码。

然后我们切换到另一个工具 BundlePhobia。这个工具允许你输入任何 NPM 包的名称,并实际看到它经过压缩和 gzip 后的预计大小。我们找到了一个很好的替代方案,我们使用的 slug 模块只有 2.2kb,所以我们用了它。

这对我们的性能产生了很大的影响。在此更改和发现其他减少 JavaScript 包大小的机会之间,我们节省了 2.1mb 的代码。

综合考虑到这些 bundle 的压缩和缩小尺寸,我们总体上看到了 65% 的提升。我们发现这确实值得去做。

因此,总的来说,尝试去消除你的网站和应用中不必要的下载吧。对资源进行清点并衡量其性能影响,这可能会带来焕然一新的改变,因此请确保定期审查你的资源。


通过代码拆分降低 JavaScript 启动时间

虽然大型网络负载可能对我们的应用程序产生重大影响,但还有另一样东西也能产生巨大影响——那就是 JavaScript。

JavaScript 是你最昂贵的资产。在移动设备上,如果您发送了大量的 JavaScript,它可能会延迟用户与界面组件交互的时间。这意味着他们点击 UI 的操作可能是毫无意义的。因此,了解 JavaScript 成本为何如此之高显得极为重要。

这是浏览器处理 JavaScript 的方式。

图10. JavaScript 处理

我们首先必须下载该脚本,我们的 JavaScript 引擎需要解析该代码,编译并执行它。

现在,这些(处理)阶段在台式机或笔记本等高端设备、甚至是高端手机并不用花很长时间。但在中等配置手机上,这个过程可能要花上 5 到 10 倍的时间。这正是延迟交互的原因,因此把它降下来非常关键。

为了帮你发现 App 的这些问题,我们向 Lighthouse 引入了一个新的 JavaScript 启动时间审查工具。

图11. JavaScript 启动时间审查

在 Oodle App 的例子中,它显示我们花了 1.8 秒来启动 JavaScript。而这段时间所做的,是将所有路由和组件静态导入到一整个 JavaScript 包中。

解决此问题的方法之一是使用代码分割。

代码分割的概念是,不要一次把一整个披萨的 JavaScript 给到你的用户,不妨一次给只他们所需要的一片?

代码分割可以应用于路由级别或组件级别。它适用于 React 和 React Loadable,Vue.js,Angular,Polymer,Preact 以及其他多个库。

我们将代码分割合并到我们的应用中,从静态导入切换为动态导入,实现了我们所需的异步懒加载代码。

图13.使用动态导入的代码分割

这种影响既缩小了 bundle 的大小,也节省了我们的 JavaScript 启动时间。它把时间降到了 0.78 秒,为应用提速 56%。

通常,如果你正在构建你的 JavaScript 体验,请务必做到仅向用户发送他们所需要的代码。

利用代码分割等概念,发掘 tree shaking 等思路,查看 webpack-libs-optimizations 仓库,了解在使用 webpack 时如何减小库的体积的方法。



优化图片

图片加载性能的笑话

在 Oodle App 中,我们使用了大量图片。不幸的是,Lighthouse 对它的热情远远低于我们。事实上,我们在所有三个与图片相关的审查上都挂掉了。

我们忘了对图片进行优化,没有正确处理好它们的体积,我们也本可以使用其他图片格式来优化。

图14. Lighthouse 图像审查

我们开始对图片进行优化。

针对一次性的优化,你可以使用 ImageOptim 或 XNConvert 等可视化工具。

更自动化的方法是使用像 imagemin 这样的库,在构建过程中优化图片。

这样,你就可以确保将来添加的图片自动得到优化。一些 CDN,例如 Akamai 或 Cloudinary 或 Fastly 等第三方解决方案,也提供了全面的图像优化解决方案。所以你也可以放心地把图片托管到这些服务上。

如果由于成本或延迟问题而不想这么干,Thumbor 或 Imageflow 等项目也提供自托管替代方案。

图15.优化前后

我们的背景 PNG 图片在 webpack 中被标记为大,事实的确如此。在将调整到 viewport 大小,并通过 ImageOptim 优化后,我们把体积降到了 100kb,还可以接受。

我们对网站上的图片重复执行了此操作,由此显着降低了整体页面的体积。


为动画内容使用正确的格式

GIF 的代价极其昂贵。令人惊讶的是,GIF 格式从未打算成为动画平台。因此,切换到更合适的视频格式可以大大节省文件大小。

在 Oodle App 中,我们在主页上使用了 GIF 作为介绍动画。根据 Lighthouse 的说法,切换到更高效的视频格式能节省超过 7mb。我们的动画就差不多 7.3mb 了,这对于任何网站来说都太大了,所以我们把它变成了一个包含两个源文件的视频元素——mp4 和 WebM,以兼容更多浏览器。

图16.用视频替换动画 GIF

我们使用 FFmpeg 工具将动画 GIF 转换为 mp4 文件。 WebM 格式则节省得更多——ImageOptim API 可以做这种转换。

这种转换为我们节省了超过 80% 的总体积。这让我们降到了 1mb 左右。

尽管如此,1mb 仍算是网络传输中的大型资源,特别是对于带宽受限的用户而言。幸好,我们可以使用 Effective Type API 来检测到它们处于慢网络,然后给它们提供体积更小的 JPEG。

此接口使用高效的往返时间和降低值来判断用户所用的网络类型。它只返回一个字符串,像 slow 2G,2G,3G 或 4G。因此,根据此值,对 4G 以下的用户,我们使用图像替换掉了视频元素。

它确实牺牲了一点点用户体验中,但至少在慢网络中,该站点也是可用的了。


懒加载屏幕外图片

轮播动画,滑块或非常长的页面通常会加载图像,即使用户并不能立即在页面上看到它们。

Lighthouse 将在屏幕外图像审核中标记此行为,您也可以在 DevTools 的网络面板中自行查看。如果你看到很多图像传入,但它们之中只有少数可见,则意味着你可以考虑延迟加载它们。

浏览器本身尚不支持懒加载,因此我们必须使用 JavaScript 来添加此功能。我们使用 Lazysizes 库为我们的 Oodle 封面添加懒加载行为。

Lazysizes 非常智能,因为它不仅可以跟踪元素的可见性变化,还可以主动预获取视图附近的元素,以获得最佳的用户体验。它还提供了 IntersectionObserver 的可选集成,为你带来非常高效的可见性查找。

在此更改后,我们的图片将按需提取。如果你想深入了解该主题,请查看 images.guide - 一个非常方便和全面的资源。

images.guide: https://images.guide/


帮助浏览器尽早提供关键资源

并非每个通过网络发送到浏览器的字节都具有同等的重要性,浏览器知道这一点。许多浏览器都有试探性方法来决定他们应该首先获取什么。所以有时它们会在获取图片或脚本之前获取 CSS。

可能有用的东西是作为页面作者的我们,可以告诉浏览器对我们来说真正重要的是什么。值得庆幸的是,在过去几年中,浏览器已经添加了许多功能来帮助我们实现这一功能,例如:使用 link rel=preconnect,preload 或 prefetch 实现资源提示。

这些 Web 平台的功能可以帮助浏览器在正确的时间获取正确的资源,并且它们会比使用脚本完成的一些自定义加载,基于逻辑的方法更有效。

让我们看看 Lighthouse 如何实际指导我们有效地使用这些功能。

Lighthouse 让我们做的第一件事就是避免多次与任一源的昂贵往返请求。

图17.避免多次与任一源的昂贵往返请求

对于 Oodle App,我们实际上重度使用 Google 字体。每当在页面中使用 Google Fonts 样式表时,它最多会连接两个子域。Lighthouse 告诉我们,如果我们能够预热这种连接,我们可以在初次连接时节省最多 300 毫秒。

利用 link rel preconnect,我们可以有效地屏蔽连接延迟。

特别是对像 Google Fonts 这种把字体 CSS 托管在 googleapis.com 上,以及把字体资源托管在 Gstatic 上的资源,这可能会产生很大的影响。所以我们应用了这个优化,优化了几百毫秒。

Lighthouse 建议的下一件事是预先加载关键请求。

图18.预加载关键请求

<link rel=preload> 非常强大,它通知浏览器当前导航中需要该资源,并且尝试让浏览器尽快获取它。

现在,Lighthouse 告诉我们,我们应该预加载我们的关键网络字体资源,因为我们正在加载两种网络字体。

预加载网络字体如下所示 - 指定rel = preload,将字体类型传入 as 字段,然后指定要加载的字体类型,例如 woff2。

这对你的页面产生的影响将非常明显。

图19.预加载资源的影响

通常,如果不使用 link rel preload,如果 Web 字体恰好对你的页面至关重要,那么浏览器必须做的是首先获取 HTML,解析 CSS,以及接下来的其他资源,最后才去获取你的网络字体。

而使用了 link rel preload,一旦浏览器解析了 HTML,它就可以提早开始获取这些网络字体。对我们的 App 来说,这可以减少我们使用 Web 字体渲染文本所花费的时间。

现在,如果你想尝试使用 Google Fonts 预加载字体,那还没那么简单,我们还有一个问题。

我们在样式表中的字体中指定的 Google Font URL 恰好是 Google 字体团队定期更新的内容。这些 URL 可能会过期或定期更新,因此,如果你希望完全控制字体加载体验,我们建议你自行托管你的 Web 字体。这也很棒,因为它让你可以访问 link rel preload 等内容。

在我们的例子中,我们发现 Google Web Fonts Helper 工具在帮助我们离线某些网络字体并在本地设置它们时非常有用,因此请了解下该工具。

无论你是将 Web 字体还是 JavaScript 作为关键资源的一部分,都应使浏览器尽快提供该关键资源。



实验:优先级提示

今天还有一个特别的东西要与你分享。除了资源提示和预加载等功能外,我们还在开发一种全新的实验性浏览器功能,我们称之为优先级提示。

图20.优先级提示

这项新功能允许你提示浏览器某资源的重要性。它暴露了一个新属性 - importance - 可取值 low,high 或 auto。

这使我们能降低不太重要资源的优先级,例如非关键样式,图片或 fetch API 调用,以减少流量抢占。我们还可以提升更重要事物的优先级,例如我们的英雄图片。

对于我们的 Oodle App,这实际上给了我们一个可以优化的机会。

图21.设置初识可见内容的优先级

在我们对图像设置懒加载之前,浏览器的做法是,我们将这个图片轮播与所有涂鸦一起使用,而浏览器会在轮播最开始时以高优先级获取所有图像。不幸的是,轮播中间的图像对用户来说是最重要的。所以我们的做法是,将背景图像的重要性设置得非常低,前景图像的重要性设置得非常高,这在慢速 3G 时能带来 2 秒的提速,我们也能够快速地获取和渲染这些图像。这是一个很棒的体验。

我们希望在几周内将这个功能带到 Canary,还请密切关注。



制定一个网络字体加载策略

排版是良好设计的基础,如果你使用的是网络字体,理想情况下你并不想阻塞文本的渲染,而且你绝对不想显示不可见的文本。

我们在 Lighthouse 中强调了这一点,在 avoid invisible text while web fonts are loading 审查中可以找到。

图22.在加载网络字体时避免使用不可见文本

如果你使用 font face 代码块加载网络字体,并且该字体需要较长时间才能获取到,你就是在让浏览器决定该做什么。有些浏览器会在退回到系统字体之前等待最多三秒钟,并且一旦字体下载完毕,浏览器最终又会切换到字体。

我们试图避免这种不可见文本,在这种情况下,如果网络字体加载花了太长时间,我们将无法看到本周的经典涂鸦。幸好,通过一个名为 font-display 的新功能,你实际上可以更好地控制这个过程。

Font display 可帮助你根据交换所需的时间来决定网络字体的渲染或退阶方式。

在这种情况下,我们使用字体显示交换。交换为字体提供零秒块周期和无限交换周期。这意味着如果字体加载需要一段时间,浏览器会立即使用备用字体绘制文本。一旦字体可用,它就会转换它。

对我们的 App 来说,这功能非常棒,它允许我们很早就显示一些有意义的文本,并在网络字体准备好后马上转换过去。

图23.字体显示结果

一般情况下,如果你碰巧在使用网络字体,且它占据了网络的很大一部分,那么你得有一个很好的网络字体加载策略。

有许多 Web 平台能帮你优化字体的加载体验,你还可以查看 Zach Leatherman 的 Web Font Recipes 仓库,因为它实在太棒了。

Web Font Recipes repo: https://www.zachleat.com/web/recipes/



减少渲染阻塞脚本

我们的应用中还有其他部分可以在下载链中提前推送,以便提前提供一些基本的用户体验。

在 Lighthouse 时间轴上,你可以看到在加载所有资源的前几秒内,用户无法看到任何内容。

图24.减少阻塞式渲染样式表的机会


下载和处理外部样式表阻塞了我们的渲染过程的进展。

我们可以尝试通过先提供一些样式来优化我们的关键渲染路径。

如果我们提取负责初始渲染的样式并在 HTML 中内联它们,浏览器可以直接渲染它们而无需等待外部样式表。

在我们的例子中,我们使用名为 Critical 的 NPM 模块在构建步骤中内联 index.html 中的关键内容。

虽然这个模块为我们完成了大部分繁重工作,但要让它在不同路线上顺利运行仍然有点棘手。

如果你不小心或者你的站点结构非常复杂,若你没有从一开始就规划 app shell 架构,那么引入这种模式可能非常困难。

这就是为什么在早期就考虑性能因素非常重要的原因。如果你从一开始就没有设计性能,那么之后再实施就可能遇到问题。

最终我们的风险得到了回报,我们设法让它发挥作用,App开始更早地提供内容,显着改善了我们的第一个有意义的绘画时间。


结果

这是我们应用在网站上的一长串性能优化。我们来看看结果。结果表明了我们的应用在优化之前和之后,在中等设备、3G 网络上是如何加载的。

Lighthouse 表现得分从 23 上升到 91。在速度方面取得了相当不错的进步。所有这些变化都是由我们不断检查并遵循 Lighthouse 报告推动的。如果你想了解我们如何在技术上实施所有改进,请随时查看我们的仓库(http://github.com/google/oodle-demo),尤其是 PR。


预测性能 - 数据驱动的用户体验

我们相信机器学习在未来的许多领域代表着令人兴奋的机会。我们希望将来能引发更多实验的一个想法是,真实数据可以真正指导我们正在创建的用户体验。

今天,我们对用户可能想要或需要的内容做出了很多武断的决定,由此判断什么资源该预先提取,预加载或预先缓存。如果我们猜对了,我们可以优先考虑少量资源,但很难将其扩展到整个网站。

我们实际上有数据可以更好地为我们的优化提供支持。使用 Google Analytics reporting API,我们可以查看下一个首页以及我们网站上任意网址的退出百分比,从而得出我们应优先考虑哪些资源的结论。

如果我们将其与良好的概率模型相结合,我们就会通过积极预获取内容来避免浪费用户的数据。我们可以利用 Google Analytics 数据,并使用机器学习和马尔可夫链或神经网络等模型来实现此类模型。

图25.用于Web应用程序的数据驱动捆绑

为了促进这个实验,我们很高兴地宣布一个叫做 Guess.js 的新计划。

图26. Guess.js

Guess.js 是一个专注于数据驱动Web用户体验的项目。我们希望它能激发人们探索使用数据来改善网络性能并超越它。它是完全开源的,可以 GitHub 上获取。这是由 Minko Gechev,Gatsby 的 Kyle Matthews,Katie Hempenius 和其他一些人与开源社区合作建立的。


总结

分数和指标有助于提高 Web 的速度,但它们只是手段,而不是目标本身。

我们都经历过慢网速页面加载,但我们现在有机会为用户提供更加愉悦的快速加载体验。

性能提升是一个旅程。许多小的变化可以带来巨大的收益。通过使用正确的优化工具并密切关注 Lighthouse 报告,你可以为用户提供更好、更具包容性的体验。

特别感谢:Ward Peeters,Minko Gechev,Kyle Mathews,Katie Hempenius,Dom Farolino,Yoav Weiss,Susie Lu,Yusuke Utsunomiya,Tom Ankers,Lighthouse 和 Google Doodles。

英文原文:

https://developers.google.com/web/updates/2018/08/web-performance-made-easy


“UC国际技术”致力于与你共享高质量的技术文章

欢迎关注我们的公众号、将文章分享给你的好友