大家有没有遇到过响应缓慢、卡顿崩溃的垃圾网站?遇上这类性能缺陷,脾气火爆的朋友往往果断选择:
狂点鼠标;
退出走人,拉低客源转化率;
搜索引擎排名因此下降。
三年多来,维基百科的移动版网站也深受一段 JavaScript 代码的戕害。在低端手机上,这段 JS 代码的页面加载时间可能超过 600 毫秒,大大影响了用户的交互体验。
在本文中,我们将一同了解如何通过几个简单步骤,让任务执行时间有效缩短约 50%。
在 JS 执行方面,600 毫秒似乎并不是什么无法接受的问题。但我们不妨想象这样的场景:当这段 600 毫秒的 JS 代码开始执行时,用户恰好想在加载过程中单击某个按钮。因为特定时段内浏览器的主线程只能处理一个任务,所以用户必须等待以下步骤完成后才能获得操作反馈:
JS 任务花 600 毫秒执行完毕
点击处理任务随后开始执行
浏览器执行必要的渲染步骤,最终更新页面内容
长任务可能导致视觉更新延迟,拖慢点击程序的处理速度
每个步骤都需要消耗时间,而任何超过 100 毫秒的响应速度都会给用户带来非常明确的延迟感受。正因为如此,谷歌将一切耗时超过 50 毫秒的任务定性为“长任务”,认为其会影响页面对用户输入的响应。他们甚至专门为此制定了“总阻塞时间”(TBT)的指标。
这里有两个长任务(大于 50 毫秒)——分别耗时 80 毫秒和 100 毫秒
所谓总阻塞时间,是指浏览器主线程上全部长任务在首屏内容绘制(FCP)和响应时间(TTI)之间的阻塞部分的总和。换言之,“阻塞部分”代表着每个长任务超出 50 毫秒之外的时间消耗。
计算以下示例中的总阻塞时间:
80 毫秒任务比 50 毫秒基准多出 30 毫秒,因此带来了 30 毫秒的总阻塞时间。
30 毫秒的任务不构成阻塞时间,因为其小于 50 毫秒且不属于长任务。
100 毫秒的任务比 50 毫秒基准多出 50 毫秒,因此带来了 50 毫秒的总阻塞时间。
由于总阻塞时间对应每个长任务超过 50 毫秒部分的总和,所以示例中的最终结果是 30 毫秒 +50 毫秒 =80 毫秒。
在常规移动硬件上进行测试时,谷歌建议站点的总阻塞时间应小于 200 毫秒。但维基百科上一项任务的执行就可能超过 600 毫秒——相当于总体上限的 3 倍。
我们该如何改进性能?
要降低这项指标,我们需要:
在首屏绘制和交互时间之间,减少主线程上的工作负载;
在保证工作内容不变的情况下,将长任务拆分成多个不超过 50 毫秒的小任务。
本文主要侧重第一种解决思路。
HTML 解析、绘制和垃圾收集都需要在主线程上运行,但引发总阻塞时间过长的罪魁祸首仍然是 JS 代码。毕竟有经验的前端开发者都知道,网站降速背后总有 JS 的身影。
在分析维基百科的移动站点时,我发现 _enable 方法占用了大部分执行时间。此方法负责对移动站点上的部分开展和折叠行为进行初始化。配置文件则显示,在 _enable 方法中,对 jQuery .on("click") 方法的调用同样速度很慢。
这里的.on("click") 调用负责向内容中的几乎所有链接附加点击事件侦听器,这样如果点击的链接包含哈希片段,相应的部分就会被展开。对于链接较少的短文章,这部分性能影响几乎可以忽略不计。但对于像“美国”这类的长词条,其内容可能包含超过 4000 个链接,因此在低端设备上的执行时间会超过 200 毫秒。
更糟糕的是,这种设计完全没有必要。侦听 haschange 事件的下游代码已经调用了与点击事件侦听器相同的方法。除非窗口位置已经指向链接目的地,否则点击链接会对 checkHash 方法调用两次——一次用于链接点击事件处理程序,另一次用于 hashchange 处理程序。
这种情况下,最好的方法当然是直接删除这个 JS 代码块,在几乎不影响主线程功能的前提下直接省下近 200 毫秒。
在分析过程中,请始终检查最耗时的部分,之后看看有没有能够优化或者删掉的代码。
经验之谈:加快网站速度的首选方法,永远是删除 JS 代码。
另一项性能审查显示,initMediaViewer 方法需要约 100 毫秒的执行时间。此方法负责将点击事件侦听器附加到内容中的各个缩略图处,这样点击缩略图即可打开媒体查看器:
与步骤 1 中的链接示例类似,这种向页面上各个缩略图附加事件侦听器的方法不利于性能扩展。
维基百科中的词条可能包含数千张相关图片。在这些多图页面上运行这段代码时,其执行时间可能超过 100 毫秒,必然增加页面的总阻塞时间。下面来看替代方法。
答案就是事件委托。
事件委托是一种强大的技术,允许我们将单个事件侦听器附加到单一元素,而该元素可以是大量其他元素的共同祖先。对于可以添加任意数量元素的用户生成内容,我们往往可以通过事件委托提高其执行效率。整个过程会用到事件冒泡,具体如下:
将事件侦听器附加至容器元素。
在事件处理程序中使用 event 参数,检查 event.target 属性以查看事件源。可以选择使用 event.target.closest(selector) API 来检查祖先元素。
如果该事件源就是我们所关注的元素或者其子元素,则进行处理。
更新后的代码如下所示:
我们修改了 initMediaViewer 方法,将一个点击事件侦听器附加到了包含所有图像的单一容器元素上。
在 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 翻译,未经许可禁止转载。
文章引用微信公众号"前端之巅",如有侵权,请联系管理员删除!