[译] The Cost Of JavaScript

399 阅读8分钟

原文链接:The Cost Of JavaScript

译者:kyrieliu(劉凯里)

我们在撸网站时,对 JavaScript 的依赖越来越重,与此同时,我们经常会为一些很难察觉的下发内容付出代价。在这篇文章中,我会介绍一些小规则,如果你愿意让你的网站在移动设备上的加载和可交互时间变得更短,它们会有所帮助。tl;dr: less code = less parse/compile + less transfer + less to decompress

Network

大多数开发者在考虑 JavaScript 带来的成本时,他们会考虑到下载和执行的成本。当用户的网速并不是很快时,发送更多字节的 JavaScript 意味着这将消耗更长的时间。

这确实会是一个问题,因为就算是第一世界国家,一个用户所有的有效网络连接类型也很有可能不是 3G、4G 或是 WiFi。也许你连接了咖啡店的 Wifi,但这 Wifi 很有可能是由一个 2G 的手机开的热点。你可以通过这些方法减少 JavaScript 的网络传输成本:

  • 只传输用户需要的代码, Code-spitting 就是一个有效的手段。

  • 最小化(ES5 的 Uglify,ES2015 的 babel-minify 或 uglify-es)

  • 狠狠的压缩代码。 在压缩率上,Brotli 比 Gzip 略胜一筹。压缩代码帮助 CertSimple 节省了 17% 的 JavaScript 体积,也帮助 LinkedIn 提升了 4% 的加载速度。

  • 将无用代码去除掉。 你可以使用 DevTools 的 代码覆盖率功能对页面进行检测。谈及代码剥离,就少不了 tree-shaking,它是 Closure Compiler 的一种高级优化,并提供了像 lodash-babel-plugin 或 Webpack 的 ContextReplacementPlugin 这样的库和插件。同时使用 babel-preset-env 和 browserlist 可以避免重复转换那些现代浏览器已经支持的 feature。更有经验的开发者会对 Webpack 的编译包进行慎重的分析,目的是将那些没用的依赖从工程中剔除掉。

  • 使用缓存从而减少网络请求。 对脚本的最长可用时间进行最优的规划(max-age)& 使用验证 token(ETag)避免重新传输未曾改动过的内容。Service Worker 的缓存可以使你的应用减少对网络条件的依赖 & 如果你想更进一步的话,V8 的 code cache 了解一下?

Parse / Compile

完成了下载以后,我们即将迎来 JavaScript 最重的成本之一:JS 引擎解析并编译代码所带来的时间消耗。在 Chrome DevTools 中,解析和编译的过程将展示在 Performance 面板中黄色“Scripting”的部分。

其中,自底向上的调用栈方便开发者查看 JavaScript 解析和编译的精确计时。

那么问题来了,这真的很重要吗?

如果在代码的解析和编译上花费大量时间,那么必然会在很长的一段时间内用户无法和你的网站进行交互。

Byte-for-byte, JavaScript is more expensive for the browser to process than the equivalently sized image or Web Font —— Tom Dale

与 JavaScript 相比,处理同样大小的图片资源也需要付出很多代价,但在平均水平的移动设备上,JavaScript 更可能对页面的可交互性造成负面的影响。

当我们在讨论解析和编译的慢速时,前提很重要:我们讨论的对象是移动设备的平均水平(别老拿iPhone说事儿)。就一般水平而言,用户手机的 CPU 和 GPU 性能是相对较低的,没有 L2/L3 cache(这里不知道怎么翻译 Orz),内存甚至也像受限般的低。

网络的状况和手机的性能往往不成正比。一个有着出色的光纤连接的用户不一定也具备同样高性能的手机去解析和执行 JavaScript,当然也有可能是相仿的情况,一个搭载强劲 CPU 的手机却受着糟糕网速的煎熬。 —— Kristofer Baxter, LinkedIn

在 JavaScript Start-up Performance 这篇文章中,我记录了解析大小约为 1MB 的未压缩的 JavaScript 在低端机和高端机上的表现。在解析和编译代码这一步骤中,最快的手机和平均水平的手机有2-5倍的差距。

举个实例来看看,比如 CNN.com?在相对高端的 iPhone8 上面,花费了大约 4s 的时间去解析和编译 CNN 的 JavaScript,而在平均水平的手机上,比如 Moto G4,却花费了大概 13s 的时间。手机性能已经显著影响了一个用户到底需要等待多久才能和网站交互这件事情。

所以说,在一般水平的机型上(比如 Moto G4)测试是一件很重要的事情,而不是只用你口袋里的手机做测试。在讨论问题这件事情上,上下文永远是一个不可少的前提:针对你用户的设备和网络情况做优化。

专业的分析可以让你更清楚那些访问你网站的用户使用的是什么类型的移动设备。这可以让开发者更好的理解那些真实的 CPU/GPU 限制给用户带来的体验。我们真的向用户下发了太多的 JavaScript 吗?Emmmm,可能是的 :) HTTP Archive 可以帮助我们分析 JavaScript 在移动端的表现,研究表明,大概 50% 的网站需要 14s 的时间才能达到可交互的状态。单单是解析和编译 JavaScript,这些网站就花费了高达 4s 的时长。

考虑到处理 JavaScript 和其他资源所要占用的时间,在页面完全准备好之前让用户等待一段时间,或许是一件司空见惯的事情。但针对这点,我们是可以做到更好的。把不重要的 JavaScript 从你的页面上移除掉,可以在减少脚本的传输时间的同时,也降低由 CPU 密集型解析所带来潜在的内存开销。这会帮助你的页面更快的达到可交互的状态。

Execution Time

除了解析和编译之外,还存在其他有成本的事情。JavaScript 的执行(在解析和编译之后执行代码)是需要占用主线程的操作之一。如果代码的执行占用了太长时间,也会阻碍网页达到可交互的状态。

If script excutes for more than 50ms, time-to-interactive is delayed by the entire amount of time it takes to download, compile, and excute the JS —— Alex Russell

为了解决这个问题,可以把 JavaScript 划分为一些小的块,避免将主线程锁死。你可以沿着这个思路针对执行这一过程做一些优化。

Patterns for reducing JavaScript delivery cost

当你试图解决由解析编译或是网络传输导致的脚本过慢的情况时,有一些模式可以帮到你,比如基于路由的分块和 PRPL。PRPL 通过堪称激进的 code-splitting 和缓存这些手段提升网页的可交互性。

让我们把这可能产生的影响更加具象化一些。我们分析了目前较为流行的移动端网站和运行在 V8 调用栈的 PWA。正如我们看到的那样,在所有的网络时间消耗中,解析时间(橘黄色)是一个值得注意的部分。

Wego,一个采用了 PRPL 的网站,实现了页面到达可交互状态的最快速度。许多网站多利用 code-splitting 尝试将它们的 JS 成本降到最低。

Other costs

JavaScript 在其他方面也会影响页面的性能:

  • 内存。由于垃圾回收机制的存在,页面可能会出现卡顿的情况。当浏览器进行内存的回收时,JS 的执行被暂停,因此频繁的垃圾回收会致使超出我们预料之外的频繁的脚本暂停。避免内存泄漏和频繁的GC暂停,以保持页面的流畅。

  • 在运行时,长时间运行的 JavaScript 会阻塞主线程,导致页面无法响应。将工作分成更小的部分(使用 requestAnimationFrame() 或 requestIdleCallback() 进行调度)可以很大程度上降低这些问题出现的概率。

Progressive Bootstrapping

因为交互的昂贵,很多网站优化了内容的视觉效果。为了在有很大 JavaScript 包的情况下快速渲染出首屏,开发者们通常会采用服务端渲染,在等到页面获取到 JavaScript 后再将页面“升级”为具有 Event Listener 的可交互页面。在这种情况下,依然需要当心 —— 这种方式也有相应的成本。你 1)通常会下发一个体积较大的 HTML 文件,这同样会延迟页面达到可交互状态,2)让用户置身于一个尴尬的境地,在 JavaScript 彻底执行完之前,页面只能看不能用。Progressive bootstrapping 会是一个更好的选择。下发一个最小的可用页面(由必要的 HTML/JS/CSS 组成)。当更多的资源达到页面以后,整个页面可进行懒加载以解锁更多的功能。

有一条准则是:尽量保持页面加载的资源都物尽其用,减少多于资源的加载,PRPL 和 Progressive Bootstrapping 都是可以帮助开发者们实现这一准则的模式。

Conclusion

传输内容的大小在网络状况一般时需要优先考虑。解析时间对于性能一般的终端设备会有较大的影响。开发者应该权衡这两种情况,争取做到最优。