5

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

 1 year ago
source link: https://www.techug.com/post/win-or-lose-in-300-milliseconds-wikipedia-s-total-blocking-time-optimization-approach2421b5324602eb1e4a8c/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
O1CN01B5VxzS1OQdsdoIXGB_!!2768491700.jpg_640x640q80_.webp
win-or-lose-in-300-milliseconds-wikipedia-s-total-blocking-time-optimization-approach2421b5324602eb1e4a8c.01.jpeg

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

  • 狂点鼠标;

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

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

三年多来,维基百科的移动版网站也深受一段 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

O1CN01Ap1lrp1OQdys3LRkV_!!2768491700.jpg_640x640q80_.webp

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK