我们可以通过减少 JavaScript 和 CSS 的体积,优先渲染页面基本元素,懒加载非首屏的内容,异步加载 JavaScript 脚本,或者考虑使用 Web Workers 处理计算密集型的任务等方法来最小化主线程的工作,提高页面性能,重点优化关键渲染路径。
浏览器为了显示页面,需要在主线程完成大部分工作。
如果主线程阻塞了,很多重要工作都没法完成,这会延长页面加载时间,使得页面没法交互,带来糟糕的用户体验,最终导致用户流失。
所以最小化主线程的工作可以提高浏览器的渲染速度,提升用户体验,具有很大的意义。
主线程阻塞的原因
导致主线程阻塞的原因主要是长时间运行的 JavaScript。
JavaScript 是非常昂贵的资源,它通常是造成主线程阻塞的罪魁祸首。
默认情况下,所有 JavaScript 代码都运行在主线程。你可以把它想象成工作日早高峰的交通,所有车辆都行驶在同一主干道上。当主干道堵塞的时候,人们将拥有非常糟糕的通勤体验,可能会疯狂按喇叭,甚至可能导致上班迟到。同样地,当浏览器主线程阻塞的时候,用户将愤怒地点击按钮,将体验低质量的动画和页面滚动延迟。
由于 JavaScript 代码可能来自许多不同的地方,开发框架、第三方插件等。所以当你的项目引入新的插件前后,测试网站的性能就显得非常重要。
需要注意的是,除了 JavaScript 代码,CSS 代码也会导致主线程阻塞,虽然没有像 JavaScript 代码阻塞主线程那么严重,但我们也需要避免 CSS 代码阻塞页面渲染。
调试主线程问题
Google’s PageSpeed Insights (PSI) 是一款不错的测试性能的工具。(估计需要翻墙,你也可以打开谷歌浏览器的开发者工具的 Lighthouse 面板)
最大限度减少主线程工作面板分析了网站主线程的活动,并将它们按组分成不同的任务。这为我们开启页面性能工作提供了一个不错的开始。
应避免出现长时间运行的主线程任务面板通过展示各具体的 JavaScript 任务的运行时长,为我们提供了更加详细的信息。长任务是指那些运行时间超过 50ms 的任务,它们同时也是导致页面崩溃的罪魁祸首。
谷歌浏览器的 Coverage 面板可以查看具体的 JavaScript 文件中有哪些没有使用到代码,以及它们的占比。
有很多工具可以帮助我们查看和测试网站的性能,要想解决问题,首先要知道问题在哪里。
如何最小化主线程工作
通过最大限度减少主线程工作的面板可以看到,有下列几种类别的任务可能导致主线程负载。
- 脚本评估
- 样式和布局
- 解析 HTML 和 CSS
- 脚本解析和汇编
- 垃圾回收
让我们仔细看看每个类别,看看有哪些方法可以解决这些问题。
延迟加载或者删除第三方 JavaScript 脚本
第三方脚本通常代码量较大。任何大量的 JavaScript 代码都会影响网站性能。第三方脚本最大的问题就是它们超出了你的控制,它们会带来一些额外的问题。
- 网络请求:向多个服务器(第三方)发送过多的请求会减慢速度。对于安全连接来说,这个时间甚至更长,这可能涉及DNS查找、重定向和多次往返到处理用户请求的最终服务器。
- 渲染:如果在关键渲染路径同步地渲染第三方脚本的代码,将会延迟文档其余元素的解析。
PageSpeed Insights 工具也为我们提供了对页面性能有影响的第三方脚本的数据,包括传输大小和阻塞主线程的时间。
你可以使用工具来阻止这些第三方脚本,看看没有它们,页面的加载性能。
打开谷歌开发者工具,转到 Network 面板并找到有问题的资源,右键单击并选择 Block request URL。
如果没有看到太大的差异,可尝试减少被阻止的 URL 列表,直到找到导致延迟的 URL。
一旦找到阻塞页面渲染的脚本,应该向这些脚本添加 defer 或 async 属性。这两个属性都使脚本非阻塞,从而减少了它们的影响。但是,它们也有重要的区别:
- 带有 defer 属性的脚本保持它们的相对顺序。浏览器不会等待它们呈现页面,而是按顺序执行它们。例如,假设我们有两个加了 defer 属性的脚本,按照脚本 A 和脚本 B 的顺序。浏览器将始终先执行脚本 A,即使脚本 B 先下载。延迟脚本也会在 DOMContentLoaded 事件之前执行。换句话说,它们仅在超文本语言标记加载和解析后运行。
- 具有 async 属性的脚本是完全独立的。无论哪个先加载,都将首先执行。它们也独立于 DOMContentLoaded 事件运行,即使文档尚未完全下载,它们也可以执行。
由于这些差异,需要 DOM 或者执行顺序很重要的脚本应该使用 defer 属性。而像广告、分析、谷歌防刷或其他相对独立的脚本通常应该使用 async。
使用 Web Workers
另一种提高网站性能的方法是使用 Web Worker。Web Worker 是指在单独的线程中执行代码,从而减少对主线程的影响。
一般来说,将非 UI 操作从主线程移开是一个很好的做法。
然而,这说起来容易做起来难。虽然所有主要的浏览器都支持它们,但使用 Web Workers 需要对 Web 和JavaScript 的工作原理有深入的技术理解,学习门槛较高。
要深入了解该主题,请查看 Surma 的 2021年 Web Workers 现状 的文章。
Code splitting
Code splitting 代码拆分,将 JavaScript 代码拆分成更小的包,按需加载,减少主线程的负担。Webpack 和 Rollup 等工具都提供了模块拆分的功能。
上面说的删除未使用的第三方脚本也适用于我们自己的 JavaScript 代码。我们同样可以使用 DevTools 查找未使用的 JavaScript 代码,如上面的屏幕截图所示。即使是一行低效的 JavaScript 代码,也可能使得网站显著变慢甚至无响应。
压缩 JavaScript 和 CSS 代码
代码压缩是页面速度优化的最佳实践。
压缩JS(或最小化JS),从代码文件中删除不必要的元素,如注释、空格和换行符或者通过应用不同的压缩算法来重写文件的二进制代码,使用比原始代码更少的位。除此之外,最小化 CSS 和最小化超文本标记语言,也有助于加快代码解析。
很多打包工具,如 Webpack 都提供了代码压缩的能力。我们可以使用 DevTools 查看文件是否被缩小和压缩。缩小的文件通常在其名称中带有 .min。压缩文件具有内容编码标头,通常带有 gzip 或 br 值。
减少项目中不必要的插件
大部分插件都有大量的 JavaScript 代码,首先检查项目中的插件对页面性能的影响,如果严重阻塞页面渲染的话,看能否使用可替代的较轻便的插件。