2019年JavaScript性能优化解析

Posted 前端之巅

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2019年JavaScript性能优化解析相关的知识,希望对你有一定的参考价值。


作者 | Addy Osmani
译者 | 王强
编辑 | Yonie
在日前的 PerfMatters 2019 大会上,Addy Osmani 发表了《javascript 性能开销》的演讲,本文整理内容如下。

原演讲视频连接: https://youtu.be/X9eRLElSW1c

过去几年来,浏览器解析和编译脚本的速度已经有了显著提升,这也改变了 JavaScript 的性能开销结构。到了 2019 年,处理脚本的主要性能开销体现在脚本下载和 CPU 执行时间上。

当浏览器的主线程忙于执行 JavaScript 脚本时可能会拖累用户交互操作,因此加快脚本执行速度并消除网络瓶颈能明显改善用户体验。

实用的高层级指南

对 Web 开发者来说上述事实意味着什么?首先,解析和编译工作 不像以前那么慢了。现在开发者做优化时,针对 JavaScript 包需要关注三大重点:

 减少下载时间

  • 控制 JavaScript 包的大小,面向移动设备时尤其要注意。较小的包可提升下载速度、降低内存使用率并减少 CPU 开销。

  • 不要只做一个大包;如果你的包大小超过 50-100kB,就把它拆分成几个小包。(通过 HTTP/2 多路复用可以同时传输多个请求和响应消息,从而减少额外请求的开销。)

  • 在移动设备上尽量缩减包的大小,这主要是考虑到网络带宽,同时也有助于降低内存使用率。

 缩短执行时间

  • 尽量避免持续占用主线程、影响页面响应速度的长任务。现在脚本下载后的执行时间是主要的性能开销之一。

 避免使用大型内联脚本

因为它们仍需在主线程上解析和编译。可以参考一条经验法则:如果脚本超过 1kb 就不用内联(这也是因为超过 1kB 时针对外部脚本的代码缓存就会启动了)。

为什么要关注下载和执行时间?

为什么我们应该关注下载和执行时间的优化工作?因为在低端网络中下载时间是影响很大的指标。尽管全球范围 4G(甚至 5G)网络正在普及,但很多人的有效连接类型(详见下方链接)依旧存在很多起伏;很多时候我们出门在外会感到网速下滑到 3G(甚至更糟)的水平上。

有效连接类型: https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/effectiveType

JavaScript 执行时间在低端手机上也有很大的影响。不同手机的 CPU、GPU 和散热限制差异巨大,所以低端和高端手机之间有着显著的性能差距,严重影响 JS 这种 CPU 密集任务的性能表现。

数据显示,在 Chrome 之类的浏览器中加载页面时,JS 的执行时间可以占到加载总耗时的最多 30%。下图是一台高端桌面 PC 从具有典型负载的网站(Reddit.com)中加载页面的性能分析:

2019年JavaScript性能优化解析

在移动端,典型的中端手机(Moto G4)执行 Reddit 的 JS 脚本耗时足足是高端手机(Pixel 3)的 3-4 倍之久,而低端手机(售价低于 100 美元的阿尔卡特 1x)的耗时更是有 6 倍之久:

2019年JavaScript性能优化解析

注意:Reddit 的桌面和移动端版本不一样,所以两个平台的性能表现无法直接比较。

如果你要着手优化 JS 脚本的执行时间,请留意可能长时间独占 UI 线程的长任务。就算页面看起来已经准备就绪了,这些长任务也可能拖累关键任务的执行。你可以把这些长任务拆分开来,并安排好各个小任务的加载优先级,这样就能加快页面响应并降低输入延迟。

2019年JavaScript性能优化解析

V8 引擎的解析 / 编译改进

相比 Chrome 60 版本,现在 V8 引擎的 JS 解析速度提高了两倍。Chrome 还做了一些优化工作让解析和编译工作并行化,现在这部分性能开销已经不再是影响体验的关键因素了。

V8 将解析和编译任务转到了 worker 线程上,将主线程上的解析和编译工作量平均减少了 40%(Facebook 上为 46%,Pinterest 为 62%),最高达到 81%(YouTube) 。这是在已有的改进工作基础上得到的性能提升数字。

2019年JavaScript性能优化解析

还可以对比不同版本 V8 引擎的性能表现。可以看到 Chrome 61 解析完 Facebook 的 JS 脚本时,Chrome 75 已经解析完 Facebook 和 6 个 Twitter 的 JS 脚本了。

2019年JavaScript性能优化解析

下面来深入了解一下这些优化的细节。简而言之,现在脚本资源可以在 worker 线程上流式解析和编译,这意味着:

  • V8 可以在不阻塞主线程的情况下解析并编译 JavaScript。

  • 当整个 html 解析器遇到<script>标记后就开始流式处理。遇到阻塞解析器的脚本时 HTML 解析器暂停,遇到异步脚本时继续。

  • 实际使用中,大多数网络条件下 V8 的脚本解析速度都比下载更快,所以脚本下载完毕后几毫秒之内 V8 也完成了解析和编译工作。

具体来说,较老版本的 Chrome 会在脚本下载完毕之后才会开始解析,这种方法很简单,但并没有充分利用 CPU 能力。从 41 到 68 版,Chrome 会在下载开始时立即在单独的线程上解析异步和延迟脚本。

2019年JavaScript性能优化解析

到了 Chrome 71,我们改成了基于任务的设置方案,让调度程序同时解析多个异步 / 延迟脚本。于是主线程解析时间缩短了约 20%,在真实网站上测得的 TTI/FID 总体上提高了约 2%。

2019年JavaScript性能优化解析

在 Chrome 72 中,我们开始使用流式传输处理主要的解析任务:现在常规的同步脚本(内联脚本除外)也会流式处理。当主线程需要基于任务的解析时,我们也不再取消这些解析操作了,从而减少了不必要的重复劳动。

旧版 Chrome 支持流式解析和编译,其中来自网络的脚本源数据必须在转发到流传输器之前进入 Chrome 的主线程。

结果经常出现的一种情况是,虽然数据已经从网络传输过来了,但是主线程忙于其他任务(如 HTML 解析、布局或 JavaScript 执行等),来不及处理这些数据,所以数据还没有转发到流任务上,流解析器只能干等。

现在我们正尝试在预加载时开始解析,以前主线程反弹会阻碍这种操作。Leszek Swirski 在 BlinkOn 10 上的演讲介绍了相关细节: https://youtu.be/D1UJgiG4_NI

DevTools 中的改进

此外 DevTools 中也存在一个问题,它在呈现整个解析任务时会表明自己正在占用 CPU(完全阻塞),但不管解析器是否需要数据(数据需要通过主线程)都会阻塞。当我们从单个流线程转向多个流传输任务时这个问题变得非常明显。下图是 Chrome 69 中的情况。

2019年JavaScript性能优化解析

DevTools 呈现解析任务时表明自己正在占用 CPU(完全阻塞)

如图,“解析脚本”任务需要 1.08 秒时间。但是解析 JavaScript 其实没那么慢才对!大部分时间都是在干等数据通过主线程而已。

Chrome 76 显示的内容就不一样了:

2019年JavaScript性能优化解析

在 Chrome 76 中,解析工作被分解为多个较小的流任务。

一般来说,DevTools 性能窗格非常适合从宏观层面分析你的页面。如果你需要了解更具体的 V8 性能指标(如 JavaScript 解析和编译时间),我们建议使用 Chrome 跟踪和运行时调用统计(RCS,https://v8.dev/docs/rcs)。在 RCS 结果中,Parse-Background 和 Compile-Background 会告诉你在主线程之外解析和编译 JavaScript 所花费的时间,而 Parse 和 Compile 是针对主线程的指标。

2019年JavaScript性能优化解析

这些改进对现实应用有多大影响?

下面来看一些真实网站的示例以及脚本流的效果。

2019年JavaScript性能优化解析

Reddit.com 有几个超过 100kB 的 JS 包,它们包装在外部函数中,为主线程带来了大量懒编译操作。如上图所示,主线程耗时会严重影响交互体验。Reddit 的大部分时间都花在了主线程上,而 worker/ 后台线程的使用率很低。

懒编译:https://v8.dev/blog/preparser

想要做优化的话,他们可以将一些大包拆分成一些不用包装的小包(比如每个包 50KB),这样每个包可以分别流解析和编译,并在载入期间减少主线程的解析和编译时间。

2019年JavaScript性能优化解析

然后是 Facebook.com。Facebook 使用了 292 个请求,加载了大约 6MB 的压缩 JS 脚本,其中一些是异步的,一些是预加载的,还有一些是低优先级的。他们的许多脚本都不大,粒度也很小,所以能并行流解析和编译,改善 Background/Worker 线程上的整体并行化表现。

但要注意的是,像 Facebook 或 Gmail 这样的老牌应用在桌面端使用这么多脚本还比较合理,但你的网站可能并不是这种情况。不管怎样还是要尽量简化 JS 包,没什么必要的就不要加载了。

虽然大多数 JavaScript 解析和编译工作都可以在后台线程上流式处理,但有些工作还是要跑在主线程上。主线程繁忙时页面就无法响应用户输入了。请密切关注下载和执行代码的操作对用户体验的影响。

注意:目前,并非所有 JavaScript 引擎和浏览器都实现了脚本流这个加载优化方案。但我们仍然相信本文能帮助大家提升整体的应用体验。

解析 JSON 的开销

JSON 语法比 JavaScript 简单很多,所以前者的解析效率也要高得多。基于这一点,web 应用可以提供大型的类似 JSON 的对象字面量(诸如内联 Redux 存储),取代将数据内联为 JS 对象字面量的做法来提升加载速度,如下所示:

const data = { foo: 42, bar: 1337 }; // 

以上是关于2019年JavaScript性能优化解析的主要内容,如果未能解决你的问题,请参考以下文章

javascript性能优化总结

JavaScript性能优化5——JSBench工具的使用

JavaScript性能优化5——JSBench工具的使用

JavaScript 性能优化技巧分享

JavaScript 性能优化技巧分享

高性能的 JavaScript 代码,优化技巧分享