Web 性能优化及相关故事 — upgrad.com
2021 年 1 月,我们在 upGrad 审视了网站的速度评分,并承认它们确实需要大幅改进。Lighthouse 是一个流行且非常有用的工具,用于衡量 Web 性能,而 upgrad.com 的评分勉强超过个位数。情况并不乐观,但对我个人而言却很兴奋,因为它提出了一个需要解决的实际问题,而本文就是关于在 Web 性能优化的惊涛骇浪中航行的历程。
太长不看版: 我们成功地将网站性能从移动端和桌面端分别约 5 分和 15 分提升到了 50 分和 70 分左右。我希望这些数字能鼓励你阅读整篇文章,但我也尽力让它读起来有趣。文章结尾见!
我认为 Web 开发这个领域对两方面都至关重要:对从事网站/应用程序开发的开发者,以及对拥有产品的企业/个人。当你同时扮演这两个角色时,情况会更好,比如当你启动你的业余项目时。Web 性能是一个既对技术人员而言有趣,又对业务方面有直接影响的领域。如果你经常运行 Lighthouse 测试(现在是时候自动化它们了),你可能已经看到过这些事实展示。
速度确实很重要。作为互联网用户,我们深知等待页面加载是多么令人沮丧。我有时会同情某些加载缓慢的网站的 Web 开发者,并多等几秒钟。不幸的是,现实世界的用户不会这么做。随着一切都变得越来越即时,在网络上提供最佳体验是 Web 开发者的核心责任之一。
2021 年 1 月
我不想说得太诗意,但让我们回到 2021 年 1 月。upGrad 的增长团队这个月完成了一个关键项目——将认证逻辑从网站中分离出来,并集成基于手机 OTP 的登录+注册流程。这是一个有趣的项目,Akshay(我的队友)很快就会写一篇文章来介绍它。但由此,现在 upgrad.com 网站只包含展现性内容。在理想情况下,本可以用纯 HTML 和 CSS 构建网站,但使用像 Nuxt.js 这样的框架给我们带来了很多在可操作性和动态性方面的好处,因为我们有内部 CMS 和编辑人员。
无论如何,网站的 Lighthouse 评分如下(桌面端):
桌面端的 17 分通常意味着你的移动端分数会更低,大约在 8-10 分之间。上面截图中需要注意的值是核心 Web 指标的数值,大部分是红色的。Lighthouse 认为这些值是重要的指标,代表你的网站如何加载以及用户如何感知它。让我们先绕个小弯,了解一下浏览器如何加载页面,这样核心 Web 指标就会清晰得多。
页面的旅程
浏览器从服务器接收到第一个 HTML 数据块时,通常会经历上述流程。HTML 是一种特殊的语言,它很宽容。因此它的解析也很特殊。稍微深入一下细节,HTML 不像大多数语言那样具有上下文无关语法。这种语法规定了语言中什么是有效的,什么是无效的。在 HTML 中,如果某些部分有错误,浏览器通常会进行纠正。除此之外,HTML 在解析过程中还可以改变!JavaScript 可以使用 document.write() 向页面注入 HTML,因此 HTML 解析被称为可重入的。
如果我们考虑一个简短的示例页面的 HTML:
场景 1
|
|
在这里,解析器将从顶部开始,继续识别相应的节点。它将找到 open html -> open body -> open h1 -> text Hello -> close h1 -> close body -> close html。在解析结束时,通常会得到一个语法树,它将映射到相应的 DOM 树。这个对象将是暴露给 JavaScript 中 document 的那个。
本质上,DOM 树将被构建,布局将完成,然后位图将被绘制到屏幕上。超级优化。
场景 2
|
|
让我们考虑这个例子,它向页面添加了一个远程 CSS 样式表。页面上的任何形式的 CSS 都是渲染阻塞的。这意味着在所有 CSS 解析完毕之前,渲染器不会绘制页面——请记住上面的流程图,渲染树构建需要 CSS 树准备就绪。
因此,在这个例子中,DOM 树将被构建,但在 CSS 文件完全加载和解析之前,渲染不会发生。不太理想。
场景 3
|
|
让我们在头部添加一个 <script> 标签。页面上的任何形式的 JavaScript 通常是解析器阻塞的。在脚本执行完毕之前,HTML 解析会被阻塞——这同样是因为 JS 有可能修改 DOM,所以浏览器不希望继续解析,然后才知道它解析的代码已经过时了。你可以想象一下,如果你有远程的 JavaScript 文件(通常情况如此),这可能会受到怎样的影响。文件必须首先下载,然后执行,解析器才能继续前进。非常不理想。
Mozilla Developer Network 文档以及 Google 的 web.dev 文章对此有非常详细的解释。你可能想看看那些。
上面绕道讨论的关键是探讨什么可能会阻碍我们的网站被加载/对用户可见。从讨论中我们可以得出两个要点:
- CSS 是渲染阻塞的
- JavaScript 是解析器阻塞的
这两点构成了大量可以执行以加速网站的优化的基础。核心 Web 指标也因为这些而受到影响。
任何渲染阻塞的东西都会恶化 LCP。任何在页面加载时处理繁重的东西都会恶化 FID,例如主线程上的同步 JavaScript 执行。任何写入文档并因此大幅改变页面布局的东西都会恶化 CLS。
在初始阶段,我们的目标是降低 LCP。
识别改进领域和制定预算
要解决性能问题,通常可以先从内部审视(相当有哲理)。在对系统进行复杂的更改之前,必须尝试在现有设置中找到可改进的领域。很可能有很多东西需要清理和优化。至少对我们来说是这样。
但在开始任何工作之前,你如何知道该选择什么以及该更改将如何影响性能?性能分析工具登场。有许多在线工具可以帮助你准确找出瓶颈所在。Lighthouse 就是这样一种工具,但我认为它应该用于对最终分数进行基准测试。还有另一个名为 WebPageTest 的在线工具,它可以为你提供网站在各种场景下的详细性能报告。我个人发现它对于识别个别问题非常有用。
以下是来自 webpagetest.org 的截图:
上面的报告详细展示了页面上发生的情况——从核心指标到瀑布视图以及上面标记的指标。它特别有助于映射是什么延迟了某些指标,然后有针对性地处理那些资源。
制定预算 这是一个有争议的话题,但每个人都可以有自己偏好的制定优化预算的方式。这有助于我们为要为用户提供的指标或体验维持一个阈值。我将本节保持简短,并链接到在线有用资源:
- https://web.dev/defining-core-web-vitals-thresholds/
- https://csswizardry.com/2020/01/performance-budgets-pragmatically/
我们从一些明显的目标领域开始——一些我们早就知道但没有时间修复的问题。这包括一些未使用的脚本和一些可以本地加载却全局加载的模块。在 Nuxt 中,你可以定义全局插件,这些插件会被包含在主包中——如果监控不当,这通常是一种不好的做法。简而言之,以下是一些可以关注的关键领域:
懒加载所有非必要内容
影响 — 可交互时间,首次内容绘制 你必须延迟/懒加载所有对首次加载不关键且不影响 SEO 的内容。这包括模态框、非关键脚本、图片、样式表和你应用程序的代码块。
对于我们的网站,当我们懒加载所有模态框内容时,看到了显著差异。由于它本身是一个独立的模块,我们可以轻松做到这一点。大多数框架都有懒加载组件的特殊语法,例如 React 中的 React.lazy() 和 Vue 中的 import() 语法。打包器基于此语法提供自动代码分割,使你的代码块分开,只在需要时加载。
由于 Nuxt 使用 webpack,我们可以使用 webpack 的魔法注释来定义代码块的加载行为。/* webpackPrefetch: true */ 就是这样一个有用的注释,它可以预取代码块,使体验类似于同步加载。
图片懒加载是快速网站的核心,通常已经实现。但如果还没有,你应该立即实施。有流行的库可以实现这一点,现在 HTML 中也有一种原生方式,尽管并非所有浏览器都支持。
告诉浏览器延迟加载脚本或异步加载脚本的标准方法是正确使用 defer 和 async 关键字。defer 将脚本的处理移到解析步骤的末尾。脚本在后台获取,如果有多个脚本则并行获取。这解决了上面“场景 3”中提到的问题。这也是为什么通常建议将脚本标签放在 body 末尾的原因。这有助于两种方式——它允许脚本“看到”在解析器到达脚本标签之前构建的整个页面主体,其次是解析器不会在页面开头的某个地方被脚本阻塞。
async 以类似的方式帮助,但它不是在末尾加载脚本,而是异步加载甚至处理脚本。这些脚本不会等待 DOM 树的构建,而是在加载完成后立即执行。async 和 defer 的一个关键区别是,所有使用 defer 加载的脚本都会阻塞 DOMContentLoaded 事件,而使用 async 加载的则不会。
另一个可以利用框架的有用特性是脚本的动态加载。牢记核心思想——只加载构建页面所需的资源——我们可以动态地将 <script> 标签推送到网站的头部。例如,这允许仅在实际显示视频的页面上加载 YouTube 播放器脚本,确保不会减慢根本没有视频的页面的速度。
限制关键资源 — 缩短关键渲染路径
影响 — 首次内容绘制,最大内容绘制 关键渲染路径是从浏览器收到响应(HTML)的第一个字节到页面开始变得可见的过程。我们的目标应该是尽可能缩短这个过程。随着资源添加到这条路径,开始绘制的时间不断增加,影响了页面加载体验。
关键渲染路径取决于 HTML 文档的解析方式。如果只有静态 HTML,你会得到很好的 LCP 分数。但实际上,HTML 页面上通常会添加许多资源,这些资源有时对于网站正常运行至关重要。如果这些资源是你的页面渲染所必需的,那么它们就会贡献给关键渲染路径。解析器必须完成加载和执行它们的工作,然后继续解析页面,最后渲染器接管。
对我们来说,关键在于查看 webpagetest 的瀑布图快照,看看是什么延迟了首次渲染。检查后,我们发现一个特定的 CSS 文件是以关键方式加载的,更糟糕的是,它没有通过 CDN 提供。
找到这样的资源并解除对它们的阻塞,帮助我们将起始渲染时间大幅改善——从旧的 4.2 秒改善到 0.4 秒。
页面最初的 HTML 响应应仅包含关键 CSS——页面正确样式所需的 CSS。其他的都可以延迟加载。一种流行的异步加载 CSS 的方法是使用 link 标签的 media 属性。
|
|
在这里,由于 media 值是 print,浏览器中的 HTML 解析器会跳过等待此样式表并继续前进。样式表仍然会在后台加载,无论何时加载完成,它都会被解析。这防止了在 CSS 链接标签处阻塞解析器,并且对于首次加载时不需要的样式表有好处。
即使使用上述解决方案,我们也必须确保关键 CSS 是精简的。像 PurgeCSS 这样的工具可以通过遍历最终生成的页面的 HTML 来清理页面上未使用的样式,从而实现自动化。
预加载关键资源
影响 — 最大内容绘制 另一个重要的做法是预加载所有关键资源。如果你的网站在首屏有图片,你必须预加载它,以便尽快在视觉上可用。
可以使用 preload 关键字来实现这一点,许多打包器会自动为你完成。
|
|
应用这些实践在很大程度上降低了我们的 LCP——从 3.9 秒降至 0.9 秒。
Google 的 web.dev 很好地总结了上述要点——它介绍了 PRPL 模式。
- P — 推送(或预加载)最重要的资源。
- R — 尽快渲染初始路由。
- P — 预缓存剩余资源。
- L — 懒加载其他路由和非关键资源。
除了预缓存,我想我已经讨论了其余各点。预缓存通过缓存具有较长生存时间的资源,有助于提高后续加载的性能。
upgrad.com 性能的整体改进
由于网站运行在服务器端渲染的 Nuxt 上,这给性能优化带来了一些特殊的条件。任何 SSR 框架,配合 CDN 缓存,都能保证出色的 TTFB 和 LCP,但在可交互时间上会受到影响,因为前端会发生一个再水合过程。
Nuxt 中 SSR 工作原理的简要描述(来自 nuxtjs.org):
服务器端渲染是应用程序通过在服务器上显示网页而不是在浏览器中渲染的能力。服务器端将完全渲染的页面发送到客户端;客户端的 JavaScript 包接管,然后允许 Vue.js 应用程序进行水合。
考虑到这一点,我们只针对整个过程中的渲染和预加载部分。我们计划在未来转向完全静态渲染,这应该能解决水合的问题。
无论如何,我们用了 4 个月的时间进行这项练习,并偶尔处理性能相关的任务。我相信这个领域的大部分工作必须是缓慢而稳定的。当你开始时,需要相当数量的试错、研究和阅读。但是,持续努力改进然后维护性能确实会得到很好的回报。这就是我们的 LCP(在桌面设置中)随时间改善的情况:
我们的 Lighthouse 分数也有所提高,如文章开头所述。当前分数:
我非常享受这项工作,并且学到了很多关于页面如何渲染以及如何使它们变快的知识。我们的目标是达到更好的分数,因为仍有改进空间,但老实说,当前的分数已经不错,可以休息一下,用新鲜的头脑回顾一下。作为后续步骤,这是我们正在关注的:
- 性能基准测试 — 我们已经提高了分数,但现在我们的工作是维持它们在那里。持续监控是维护性能的关键部分。有现成的工具可用,或者你可以轻松地自己制作一个(我做了,稍后会链接)。
- 性能优化的组件 — 我们正在考虑用更轻量级的替代品替换某些共享的 UI 组件。
- Brotli 压缩和基础设施层面的变更 — Brotli 已启用,但在实现方面需要一些改进。此外,从基础设施角度看,我们希望减少到达源服务器所需的跳数。
如果没有我合作的团队、我的经理 Maitrey 和 Rohan,这项壮举是不可能完成的。我、Rohan 和 Maitrey 开了很多次会议来讨论这个问题,并且一直致力于构建一个高性能和高效率的平台。事情像魔法一样奏效!还要感谢产品负责人 Rohit 和 Shahir,他们将其视为直接影响业务的事情(并不断提醒我们网站速度慢的事)。最后,感谢工程负责人 Vishal 和 Puneet 始终支持整个练习。
我已经在需要时链接了相关资源,但对于所有其他资源,这里有一个汇总列表:
- https://www.smashingmagazine.com/2021/01/front-end-performance-2021-free-pdf-checklist/
- https://www.smashingmagazine.com/2021/01/smashingmag-performance-case-study/
- https://web.dev/optimize-lcp/
- https://web.dev/vitals/
- https://csswizardry.com/2020/01/performance-budgets-pragmatically/
你有类似的经验或建议吗?我很乐意讨论!你可以通过电子邮件或 Twitter 联系我,或者在下方评论。请访问 upGrad.com 查看我们完全在线的项目!如果你希望与我们充满热情的团队合作,请查看我们的招聘页面。我们一直在寻找有抱负、有才华的人!