2019年JavaScript性能优化解析

2019 年 7 月 1 日 前端之巅


作者 | 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)中加载页面的性能分析:

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

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

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

V8 引擎的解析 / 编译改进

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

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

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

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

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

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

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

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

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

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

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

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

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

DevTools 中的改进

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

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

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

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

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

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

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

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

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

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

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

然后是 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 }; // 🐌

……它可以用 JSON 字符串形式表示,然后在运行时进行 JSON 解析:

const data = JSON.parse('{"foo":42,"bar":1337}'); // 🚀

只要 JSON 字符串仅被评估一次,那么相比 JavaScript 对象字面量,JSON.parse 方法就要快得多,冷加载时尤其明显。

将普通对象字面量用于大量数据时还会带来一种风险:它们可以被解析两次!

  1. 字面量预解析时是第一次。

  2. 字面量被懒解析时是第二次。

第一次解析是必须的,可以将对象字面量放在顶层或 PIFE中来避免第二次解析。

相关链接:

PIFE:https://v8.dev/blog/preparser#pife

重复访问时的解析 / 编译情况

V8 的(字节)代码缓存优化可以改善重复访问时的体验。首次请求脚本时,Chrome 会下载脚本并将其提供给 V8 编译,同时将文件存储在浏览器的磁盘缓存中;当第二次请求 JS 文件时,Chrome 从浏览器缓存中获取该文件,并再次将其提供给 V8 编译。但这次编译的代码被序列化,并作为元数据附加到缓存的脚本文件中。

V8 中的代码缓存工作原理示意图

第三次请求脚本时,Chrome 从缓存中获取脚本文件和文件的元数据,并将两者都交给 V8 引擎。V8 会反序列化元数据来跳过编译步骤。如果前两次访问间隔小于 72 小时,代码缓存就会启动。如果使用服务 worker 缓存脚本,Chrome 也会主动启用代码缓存。详细信息可以参阅 web 开发者的代码缓存指南: https://v8.dev/blog/code-caching-for-devs

总结

到了 2019 年,加载脚本的主要瓶颈在于下载和执行脚本的时间开销。你可以为页面的顶层内容安排一个较小的同步(内联)脚本包,其余内容则使用一个或多个延迟脚本。可以把较大的包拆分成许多小包来按需加载。这样一来就能充分利用 V8 的并行化能力。

在移动设备上,为了减少网络、内存和 CPU 需求,你需要尽量减少脚本的数量。此外还应仔细调整缓存策略,让解析和编译任务尽量在主线程外执行。

 参考资料

https://v8.dev/blog/scanner

https://v8.dev/blog/preparser

英文原文: https://v8.dev/blog/cost-of-javascript-2019

 活动推荐

Serverless 技术被认为是前端开发未来趋势,WebAssembly 离我们越来越近,跨端小程序开发更加普遍,企业级应用的前端稳定性保障手段更成熟。一切的一切都在告诉我们,前端技术有太多可借鉴的经验值得学习。7 月 12 日深圳 ArchSummit 全球架构师,一网打尽所有干货。扫码或点击阅读原文了解详细信息。

全价期,报名联系票务灰灰 17326843116

登录查看更多
0

相关内容

JavaScript 是弱类型的动态脚本语言,支持多种编程范式,包括面向对象和函数式编程。
【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
57+阅读 · 2020年6月26日
【实用书】Python爬虫Web抓取数据,第二版,306页pdf
专知会员服务
117+阅读 · 2020年5月10日
TensorFlow Lite指南实战《TensorFlow Lite A primer》,附48页PPT
专知会员服务
69+阅读 · 2020年1月17日
阿里巴巴达摩院发布「2020十大科技趋势」
专知会员服务
106+阅读 · 2020年1月2日
【书籍推荐】简洁的Python编程(Clean Python),附274页pdf
专知会员服务
179+阅读 · 2020年1月1日
最新《分布式机器学习》论文综述最新DML进展,33页pdf
专知会员服务
118+阅读 · 2019年12月26日
2019年10月编程语言排行榜:前8名15年如一日
大数据技术
7+阅读 · 2019年10月13日
2019年药品零售行业全景图
行业研究报告
31+阅读 · 2019年9月18日
在K8S上运行Kafka合适吗?会遇到哪些陷阱?
DBAplus社群
9+阅读 · 2019年9月4日
2019年Q1中国互联网流量季度分析报告
艾瑞咨询
5+阅读 · 2019年5月15日
使用 C# 和 Blazor 进行全栈开发
DotNet
6+阅读 · 2019年4月15日
5GAA:C-V2X和DSRC的性能对比分析报告
智能交通技术
11+阅读 · 2019年3月8日
优化哈希策略
ImportNew
5+阅读 · 2018年1月17日
Arxiv
19+阅读 · 2019年4月5日
Doubly Attentive Transformer Machine Translation
Arxiv
4+阅读 · 2018年7月30日
Arxiv
8+阅读 · 2018年1月19日
Arxiv
5+阅读 · 2017年7月23日
VIP会员
相关VIP内容
相关资讯
2019年10月编程语言排行榜:前8名15年如一日
大数据技术
7+阅读 · 2019年10月13日
2019年药品零售行业全景图
行业研究报告
31+阅读 · 2019年9月18日
在K8S上运行Kafka合适吗?会遇到哪些陷阱?
DBAplus社群
9+阅读 · 2019年9月4日
2019年Q1中国互联网流量季度分析报告
艾瑞咨询
5+阅读 · 2019年5月15日
使用 C# 和 Blazor 进行全栈开发
DotNet
6+阅读 · 2019年4月15日
5GAA:C-V2X和DSRC的性能对比分析报告
智能交通技术
11+阅读 · 2019年3月8日
优化哈希策略
ImportNew
5+阅读 · 2018年1月17日
Top
微信扫码咨询专知VIP会员