300 毫秒分胜负:维基百科的总阻塞时间优化之道

大家有没有遇到过响应缓慢、卡顿崩溃的垃圾网站?遇上这类性能缺陷,脾气火爆的朋友往往果断选择:

  • 狂点鼠标;

  • 退出走人,拉低客源转化率;

  • 搜索引擎排名因此下降。

三年多来,维基百科的移动版网站也深受一段 JavaScript 代码的戕害。在低端手机上,这段 JS 代码的页面加载时间可能超过 600 毫秒,大大影响了用户的交互体验。

在本文中,我们将一同了解如何通过几个简单步骤,让任务执行时间有效缩短约 50%。

总阻塞时间:长任务的重要性

在 JS 执行方面,600 毫秒似乎并不是什么无法接受的问题。但我们不妨想象这样的场景:当这段 600 毫秒的 JS 代码开始执行时,用户恰好想在加载过程中单击某个按钮。因为特定时段内浏览器的主线程只能处理一个任务,所以用户必须等待以下步骤完成后才能获得操作反馈:

  1. JS 任务花 600 毫秒执行完毕

  2. 点击处理任务随后开始执行

  3. 浏览器执行必要的渲染步骤,最终更新页面内容


 长任务可能导致视觉更新延迟,拖慢点击程序的处理速度

每个步骤都需要消耗时间,而任何超过 100 毫秒的响应速度都会给用户带来非常明确的延迟感受。正因为如此,谷歌将一切耗时超过 50 毫秒的任务定性为“长任务”,认为其会影响页面对用户输入的响应。他们甚至专门为此制定了“总阻塞时间”(TBT)的指标。


这里有两个长任务(大于 50 毫秒)——分别耗时 80 毫秒和 100 毫秒

总阻塞时间是什么?

所谓总阻塞时间,是指浏览器主线程上全部长任务在首屏内容绘制(FCP)和响应时间(TTI)之间的阻塞部分的总和。换言之,“阻塞部分”代表着每个长任务超出 50 毫秒之外的时间消耗。

计算以下示例中的总阻塞时间:


  1. 80 毫秒任务比 50 毫秒基准多出 30 毫秒,因此带来了 30 毫秒的总阻塞时间。

  2. 30 毫秒的任务不构成阻塞时间,因为其小于 50 毫秒且不属于长任务。

  3. 100 毫秒的任务比 50 毫秒基准多出 50 毫秒,因此带来了 50 毫秒的总阻塞时间。

由于总阻塞时间对应每个长任务超过 50 毫秒部分的总和,所以示例中的最终结果是 30 毫秒+50 毫秒=80 毫秒。

在常规移动硬件上进行测试时,谷歌建议站点的总阻塞时间应小于 200 毫秒。但维基百科上一项任务的执行就可能超过 600 毫秒——相当于总体上限的 3 倍。

我们该如何改进性能?

如何降低总阻塞时间

要降低这项指标,我们需要:

  • 在首屏绘制和交互时间之间,减少主线程上的工作负载;

  • 在保证工作内容不变的情况下,将长任务拆分成多个不超过 50 毫秒的小任务。

本文主要侧重第一种解决思路。

步骤 1:删除不必要的 JS 代码

HTML 解析、绘制和垃圾收集都需要在主线程上运行,但引发总阻塞时间过长的罪魁祸首仍然是 JS 代码。毕竟有经验的前端开发者都知道,网站降速背后总有 JS 的身影。


 在分析维基百科的移动站点时,我发现_enable 方法占用了大部分执行时间。此方法负责对移动站点上的部分开展和折叠行为进行初始化。配置文件则显示,在_enable 方法中,对 jQuery .on(“click”)方法的调用同样速度很慢。


 这里的.on(“click”)调用负责向内容中的几乎所有链接附加点击事件侦听器,这样如果点击的链接包含哈希片段,相应的部分就会被展开。对于链接较少的短文章,这部分性能影响几乎可以忽略不计。但对于像“美国”这类的长词条,其内容可能包含超过 4000 个链接,因此在低端设备上的执行时间会超过 200 毫秒。

更糟糕的是,这种设计完全没有必要。侦听 haschange 事件的下游代码已经调用了与点击事件侦听器相同的方法。除非窗口位置已经指向链接目的地,否则点击链接会对 checkHash 方法调用两次——一次用于链接点击事件处理程序,另一次用于 hashchange 处理程序。


这种情况下,最好的方法当然是直接删除这个 JS 代码块,在几乎不影响主线程功能的前提下直接省下近 200 毫秒。

在分析过程中,请始终检查最耗时的部分,之后看看有没有能够优化或者删掉的代码。

经验之谈:加快网站速度的首选方法,永远是删除 JS 代码。

步骤 2:优化现有 JS 代码


另一项性能审查显示,initMediaViewer 方法需要约 100 毫秒的执行时间。此方法负责将点击事件侦听器附加到内容中的各个缩略图处,这样点击缩略图即可打开媒体查看器:


 与步骤 1 中的链接示例类似,这种向页面上各个缩略图附加事件侦听器的方法不利于性能扩展。

维基百科中的词条可能包含数千张相关图片。在这些多图页面上运行这段代码时,其执行时间可能超过 100 毫秒,必然增加页面的总阻塞时间。下面来看替代方法。

答案就是事件委托

事件委托是一种强大的技术,允许我们将单个事件侦听器附加到单一元素,而该元素可以是大量其他元素的共同祖先。对于可以添加任意数量元素的用户生成内容,我们往往可以通过事件委托提高其执行效率。整个过程会用到事件冒泡,具体如下:

  1. 将事件侦听器附加至容器元素。

  2. 在事件处理程序中使用 event 参数,检查 event.target 属性以查看事件源。可以选择使用 event.target.closest(selector) API 来检查祖先元素。

  3. 如果该事件源就是我们所关注的元素或者其子元素,则进行处理。

更新后的代码如下所示:


  1. 我们修改了 initMediaViewer 方法,将一个点击事件侦听器附加到了包含所有图像的单一容器元素上。

  2. 在 onClickImage 方法中,我使用 ev.target.closest(selector) API 来检查点击是否来自缩略图元素或者其子元素。如果都不是,代码会提前返回,因为这里只需要关注对缩略图的点击。如果是,则代码将处理该事件。

总结

我们分别通过两轮部署,将步骤 1 和步骤 2 中的优化发布到了生产环境。

根据维基百科的综合性能测试数据,在 Moto G(5)实机测试当中,首轮部署将总阻塞时间缩短了约 200 毫秒,第二轮部署进一步缩短了约 80 毫秒。总体而言,这两个步骤让 Moto G(5)等移动设备访问长文章时的总阻塞时间缩短了约 300 毫秒。


 维基百科通过 Moto G(5)在综合性能测试中访问“瑞典”词条

虽然仍有进一步改进空间,且查询任务仍高于建议的低端设备延迟上限,但此次优化还是取得了显著成果。为了更大程度缩短总阻塞时间,后续可能有必要将任务拆分成更多小任务。

此次试验表明,有针对性的小规模优化有望实现显著的性能改进。通过删除或优化特定代码片段,看似微小的更改也会对网站的整体性能产生重大影响。换言之,要想在所有设备上改善响应速度和浏览体验,并不一定要对代码库开展复杂且广泛的修改。有时候,小小一点调整就足以引发性能质变。

原文链接:

https://www.nray.dev/blog/300ms-faster-reducing-wikipedias-total-blocking-time/

本文文字及图片出自 InfoQ

余下全文(1/3)
分享这篇文章:

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注