JavaScript的隐形成本

1,158 阅读10分钟
原文链接: mp.weixin.qq.com

在建立那些严重依赖于 JavaScript 网站的时候,有时我们会为自己发送的内容付出一些隐形的成本。在本篇文章中,我会介绍一些可以帮助你提升网站在移动设备上加载和运行速度的实用规则。

tl;dr:

更少的代码 更少的解析 / 编译 更少的数据传输 更少的解压缩

网络

大多数开发人员考虑 JavaScript 成本的时候,考虑的都是下载和执行成本。通过线路发送的 JavaScript 字节越多,所需时间就越长,用户连接就越慢。

即使是在发达国家,这也可能是一个问题,因为用户实际上用的有效网络连接类型可能并不是 3G、4G 或者 Wifi。表面上你可能连的是咖啡店的 Wifi,但实际上连到的是只有 2G 速度的蜂窝热点。

你可以通过以下的几种方式来降低 JavaScript 的网络传输成本:

  • 只传输用户需要的代码。可用代码拆分。

  • 优化压缩代码(ES5 的 Uglify,ES2015 的 babel-minify 或者 uglify-es)

  • 高度压缩(用 Brotli~q11,Zopfli 或 gzip)。Brotli 的压缩比优于 gzip。它可以帮 CertSimple 节省 17% 的压缩 JS 的字节大小,以及帮 LinkedIn 减少 4% 的加载时间。

  • 移除无用代码。用 Chrome DevTools 代码覆盖率功能来查找未使用的 JS 代码。对于精简代码,可参阅 tree-shaking, Closure Compiler 的高端模式(advanced optimizations)和类似于 lodash-babel-plugin 的微调库插件,或者像 Moment.js 这类库的 Webpack 的 ContextReplacementPlugin。用 babel-preset-env & browserlist 来避免现代浏览器中已有的转译(transpiling)功能。高级开发人员可能会发现仔细分析 Webpack 打包(bundle)有助于他们识别和调整不必要的依赖关系。

  • 缓存 HTTP 代码来减少网络传输量。确定脚本最佳的缓存时间(例如:max-age)和提供验证令牌(Etag)来避免传送无变化的字节。用 Service Worker 缓存一方面可以让应用程序网络更加灵活,另一方面也可以让你能够快速访问像 V8 代码缓存这样的功能。长期缓存可以去了解下 Webpack 带哈希值文件名(filename hashing)。

减少向用户发送 JavaScript 代码的最佳做法

解析 / 编译

下载成功后,JavaScript绝大部分的时间都消耗在 JS 引擎对下载代码的解析 / 编译 上。在 Chrome DevTools 中,解析和编译是下面性能面板(Performance panel)中黄色“脚本”时间的一部分。

Bottom-Up/Call Tree 允许我们去确切地查看解析 / 编译所用时间:

(Chrome DevTools 性能面板下级菜单>Bottom-U。启动 V8 的 Runtime Call Stats,就能看到不同阶段的时间消耗,比如解析 / 编译所用时间。)

但是,这为什么会是个问题?

耗费很长的时间在解析 / 编译代码上,会严重延迟用户与你网站的交互时间。你发送的 JavaScript 越多,在网站实现交互前所用的解析 / 编译的时间就会越长。

即使是同样多的字节,浏览器处理 JavaScript 也会比处理等大小的图片和网页字体消耗更高的成本——Tom Dale

相比于 JavaScript,处理等字节的图片所需要的时间成本很高(因为图片仍需要解码!)但是在一般的移动设备上,反而是 JS 更有可能对页面的交互产生负面的影响。

(JavaScript 字节和图像字节耗费的时间成本不同。图像通常不会阻塞主线程,也不会在解码和光栅化的时候阻止接口进行交互。然而 JS 会因为解析、编译和执行的时间消耗阻滞交互性。)

当我们说解析和编译的速度变慢的时候,要注意具体的网络端和设备端的情况,在这里我们针对的是普通手机。普通用户所使用手机的 CPU 和 GPU 速度比较慢,没有 L2/L3 缓存,甚至可能会有内存限制

网络功能和设备功能并不总是相匹配的。有速度惊人的光纤连接的用户不一定会有最好的 CPU 来解析和评估发送到他们的设备的 JavaScript。反过来也是如此... 你可能有糟糕的网络连接,但却有快速的 CPU。 - Kristofer Baxter,LinkedIn

在 JavaScript Start-up Performance 一文中,我曾提到过在低端和高端硬件上解析~1MB 解压缩过(简单)的 JavaScript 所需要消耗的时间。市面上的普通手机和运行速度最快的手机相比,解析 / 编译代码的所用的时间会有 2-5 倍的差距

(在不同级别的台式和移动设备上解析 1MB 的 JavaScript 包(经 gzip 压缩,大小约为 250KB )。当分析解析成本时,我们需要考虑的是解压后的数据量,例如〜250KBgzip 压缩过的 JS 解压缩后约为〜1MB 的代码。)

那解析 / 编译真实网站的时间差异又会是怎样呢,比如 CNN.com 网站?

在高端的 iPhone 8 上解析 / 编译 CNN 网站的 JS 大约花费了 4 秒,相比于普通手机(Moto G4)的 13 秒左右。这可以显著地影响用户与 CNN 网站实现完全交互的速度。

(苹果公司的 A11 仿生芯片和更普通的 Android 硬件中的 Snapdragon 617 的解析时间上的性能比较

这就突出了在普通硬件(比如 Moto G4)上测试的重要性,而不仅仅是在自己恰好有的手机上测试。基于自己客户原有的设备和网络条件来进行优化是很重要的

分析可以使你更加深入了解自己真实客户访问网站所用的移动设备的级别和这些设备 CPU/GPU 的局限性。

我们真的发送了太多的 JavaScript 了吗?呃... 真有可能:)

用 HTTP Archive(qian500K 站点)来分析移动设备上 JavaScript 的状态时,我们可以看到,50%的站点需要 14 秒才能取得交互。这些网站光是用来解析和编译 JS 的时间就长达 4 秒。

考虑到获取和处理 JS 和其他资源所耗费的时间,也就不奇怪用户可能需要在页面可用之前等待一段时间了。我们绝对可以在这个方面做的更好。

从网页中删除不必要的 JavaScript 可以减少传输时间、CPU 密集型解析和编译以及潜在的内存消耗,同时也有助于加快网页的交互速度

执行时间

不光是解析和编译会有时间成本。执行 JavaScript(在解析 / 编译之后运行代码)也是需要在主线程上进行的操作之一。长的执行时间也会延迟用户与你网站的交互时间。

如果脚本执行的时间超过了 50ms,那么延迟交互的时间将会是下载、编译和执行 JS 所需时间的总和——Alex Russell

为了解决这个问题,可以将 JavaScript 脚本分为几个小块来执行,以避免锁定主线程。探索一下是否可以减少脚本执行过程中进行中工作量的可能性。

减少 JavaScript 交付成本的模式

当你试图降低 JavaScript 解析 / 编译和网络传输所用时间时,类似于基于路由分块和 PRPL 这些模式也会有用。

PRPL 是一种通过激进的(agrresive)代码分割和缓存来优化交互性的模式:

为了能将 PRPL 的影响以视觉化方式表现出来。

我们用 V8 引擎中的 Runtime Call Stats 分析了流行移动网站和 progressive Web Apps(PWA)的加载时间。正如我们所看到的,解析部分(用橙色表示)是很多网站页面加载时产生显著时间消耗的部分。

Wego 网站就使用了 PRPL 来保持较低的路由解析时间,让页面交互得以快速的进行。以上的很多站点都试图采用代码分割和性能预算来降低 JS 的消耗。

JavaScript 的其他消耗

JavaScript 还可以通过其他方式来影响页面性能:

  • 存储。页面可能会因为垃圾回收(GC,garbage colleciton),页面可能会出现画面中断卡顿(junk)和暂停。因为当一个浏览器回收内存的时候,JS 的执行也会被中止,所以经常回收垃圾的浏览器会比我们想象中的更频繁地中止 JS 的执行。在这种情况下,可以通过避免内存溢出和频繁内存回收来保持页面的流畅。

  • 在运行时,长时间的运行 JavaScript 会阻塞主线程,导致页面没有响应。这种情况下,可以将脚本的工作量分成多个小的板块(具体可用 requestAnimationFrame() 或者 requestIdleCallback() 进行任务调度)来执行,以此减少页面响应的问题。

Progressive Bootstrapping

很多网站将优化内容可视性作为保证交互性所需代价的一部分。为了在 JavaScript 有大体积包体时改善首屏性能,开发人员有时会先用服务器端渲染帮助客户提前看到页面内容,然后再在 JavaScript 最终执行完成后“升级”附加上事件处理程序。

但是值得注意的是,这样做也是有代价的。你 1)通常会发送一个更大的 HTML 响应来增加交互性,2)在一段时间内,用户会处在一半的页面交互体验缺失的奇怪状况下,直到 JavaScript 处理完成。

Progressive Bootstrapping 或许会是一个更好的处理方式。浏览器请求一个最少化的功能页面(仅由当前路由所需要的 HTML/JS/CSS 组成)。当有更多的资源请求的时候,应用程序则可以懒加载(lazy-load)和解锁更多的功能。

Progressive Bootstrapping visual by Paul Lewis

仅加载可视区域内的代码是其中的关键。PRPL 和 Progressive Bootstrapping 模式均可以用来实现这一点。

结论

传输脚本的大小对低端网络至关重要,而解析时间对于 CPU 有局限性的设备很重要。降低传输脚本的大小和减少解析消耗时间是有必要的。

有团队发现采用严格的性能预算可以成功降低他们 JavaScript 的传输和解析 / 编译的时间消耗。具体可参照 Alex Russell 的“Can You Afford It?: Real-world Web Performance Budgets” 一文中关于移动设备预算的指导。

考虑一下在我们所做的架构决策下,App 逻辑到底需要多少的 JS 代码

如果你正在建一个用于移动设备上的站点,请尽可能的在代表性硬件上开发,保持较低的 JavaScript 解析 / 编译的时间成本,并采用性能预算来确保团队对自身 JavaScript 的成本关注。

更多相关学习
  • 作者 2017 年在 Chrome Dev Summit 上的演讲视频,其中涵盖了本文内容,其后就 Pinterest 和 Tinder 这样的制作网站进行了性能案例研究:https://youtu.be/_srJ7eHS3IM

  • JavaScript 启动性能:https://medium.com/reloading/javascript-start-up-performance-69200f43b201

  • 解决网站性能危机——Nolan Lawson:https://nolanlawson.github.io/frontendday-2016/

  • 你付的起吗?真实世界的性能预算——Alex Russell:https://infrequently.org/2017/10/can-you-afford-it-real-world-web-performance-budgets/

  • 评估 web 的框架和库——Kristofer Baxter:https://twitter.com/kristoferbaxter/status/908144931125858304

  • 用 Brotli 压缩 cloudflare 的试验结果(注意,高质量的动态 Broti 可以延迟初始页面的渲染,所以要仔细评估。取而代之地,你或许会想要用静态压缩。):https://blog.cloudflare.com/results-experimenting-brotli/

  • 性能特点——Sam Saccone:https://medium.com/@samccone/performance-futures-bundling-281543d9a0d5

本文作者:Addy Osmani

原文地址: https://medium.com/dev-channel/the-cost-of-javascript-84009f51e99e

作者感谢了来自 Nolan Lawson,Kristofer Baxter 和 Jeremy Wagner 的反馈。本文由作者授权翻译并发布。