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