手记

JavaScript 的时间消耗

原文出处 Addy Osmani ,译文出处dwqs

随着我们的网站越来越依赖 JavaScript, 我们有时会(无意)用一些不易追踪的方式来传输一些(耗时的)东西. 在这篇文章中, 我会介绍一些能让你的网站在移动设备上快速加载且可交互的方式.

摘要: 更少的代码 = 更少的解析/编译(时间) + 更少的传输(时间) + 更少的解压(时间)

网络

大多数开发者考虑 JavaScript 的时间消耗时, 都会首先考虑到 JavaScript 的下载和执行消耗. 脚本传输的字节越多, 花费的时间越长, 用户连接的就越慢.

即使在网络发达的国家, 这也是需要面对的一个问题, 因为用户有效的网络连接类型不一定就是 3G、4G 或者 Wifi. 你可以连接咖啡店的 Wifi, 也可能连接上一个 2G 网络的蜂窝热点.

因而, 开发者需要想办法减少 JavaScript 在网络上的传输时间. 我这提供一些参考的方式:

  • 通过代码分割(Code Splitting), 只传输用户需要的代码.
  • 减少代码体积(对于 ES5 可以使用 Uglify; 对于 ES2015, 可以使用 babel-minify 或 uglify-es)
  • 压缩代码(可以使用 Brotli ~ q11, Zopfli 或 gzip). Brotli 在压缩比上优于 gzip. 这种方式帮助 CertSimple 网站把脚本体积减少了 17%, 并帮助 LinkedIn 节省了 4% 的脚本加载时间.
  • 移除无用的代码. 可以通过 DevTools 查看代码覆盖率情况. 对于代码分离, 可以了解 tree-shaking、Closure Compiler等高级优化方式. 对于公共库则可以使用一些代码优化插件, 如针对 lodash 的代码优化插件 lodash-babel-plugin, 可用于像 Moment.js 一类库的优化插件 ContextReplacementPlugin. 此外, 使用 babel-preset-env & browserlist 可以避免编译现代浏览器已经支持的功能. 部分更高级的开发者可能会细心分析 Webpack bundles 来帮助确定不必要的依赖.
  • 通过缓存来优化网络传输. 通过 max-age 和 Etag 等方式来缓存脚本, 减少字节的传输. Service Worker 缓存技术能使你的应用具备网络弹性, 并且能使用像 V8 code cache 一样的特性. 同时, 也可以了解下通过 文件哈希名 实现长久缓存.
解析/编译

脚本下载之后, JavaScript 最消耗时间的地方就是 JS 引擎对代码的解析/编译. 在 Chrome DevTools 的性能面板中, JS 的解析和编译是 Scripting time 中的黄色部分.

从 Bottom-Up/Call Tree 可以看到更精确的解析/编译时间.

但是, 为什么会这样呢?

花费很长时间去解析/编译代码会严重延迟用户在网站上的可交互时间. 传输的脚本越多, 在网站可交互之前, 就会花费更多的时间去解析/编译代码.

和脚本相比, 浏览器也会花费很多时间来处理同等大小的图片(图片仍需要被解码). 但是在大多数移动设备上, JS 更有可能对页面的交互性产生负面影响.

当我们谈论脚本的解析和编译很慢时, 上下文是很重要的--我们说的是普通的手机设备. 普通用户的手机是配置低配的 CPU 和 GPU, 可能由于手机内存的限制, 也没有 L2/L3 级缓存设置.

在 JavaScript 性能 一文中, 我注意到在低配手机和高配手机上解析约 1M 被解压后的脚本文件所用的时间是不同的. 对于市面上解析最快的手机和普通手机之间, 大约有 2~5x 的时间差异.

那么不同配置的手机访问 CNN.com 又会是怎么样的呢?

与普通手机(Moto G4) 需要花费约 13s 来解析/编译 CNN 网站的 JS 相比, 高配 iPhone 8 仅需要约 4s 时间.这可以显著地影响用户与该站点完全交互的速度.

这突出了测试普通手机设备(如 Moto G4)的重要性而不仅仅是你口袋里的手机设备. 然而, 上下文关系也很重要: 优化网站用户的硬件设备和网络环境.

深入分析真实用户访问你的网站所使用的移动设备类型, 这样才可能明白他们真实的 CPU/GPU 等硬件约束.

另一方面, 也需要反思我们是否真的传输了太多的脚本?

通过 HTTP Archive 分析约前 500K 网站在移动设备上传输的脚本大小, 可以发现 50% 的网站需要占据 14s, 用户才可以与网站交互, 但是这些网站仅用 4s 时间来解析和编译 JS.

在获取和处理 JS 以及其他资源所需的时间中, 用户需要在页面可交互之前等待一段时间, 这一点也不奇怪, 但我们可以在这里做得更好.

移除页面上的非关键脚本不仅能减少传输时间, 也能减少 CPU 的解析/编译时间和潜在的内存开销, 这可提高页面可交互的速度.

执行时间

不仅脚本的解析和编译需要时间, 脚本的执行也需要时间. 长时间的执行时间也会延迟用户与站点的交互速度.

如果脚本的执行时间超过 50ms, 那么可交互时间的延迟将是脚本下载、编译和执行脚本所花费时间的总和. -- Alex Russell

为减少脚本的执行时间, 可以将脚本分成小块来执行, 以避免锁住主线程. 可以考虑是否能减少脚本在执行过程中需要完成的工作量, 如果工作量很多, 就将脚本分成小块来分解工作量, 以提高页面可交互的速度.

降低 JavaScript 交付成本的模式

当你尝试着降低 JavaScript 的解析/编译和网络传输时间时, 也可以试试基于路由的代码分割或 PRPL 模式来降低 JavaScript 的交付成本.

PRPL 是一种通过代码分割和缓存来优化页面交互的模式:

通过 V8’s Runtime Call Stats, 我们可以分析一些受欢迎移动站以及 PWA 应用的加载时间. 从下图可以看出, 脚本解析所需要的时间(橙色部分)是页面加载中最耗时的一部分:

其它消耗

除上述方式外, JavaScript 还能通过如下方式影响页面性能:

  • 内存. 由于 GC(garbage collection), 页面可能会频繁的出现闪现或者卡顿. 当浏览器回收内存时, JS 的执行会被暂停, 所以 JS 被暂停执行的频率和浏览器回收内存的频率是正相关的, 因此需要避免内存泄漏和频繁的内存回收导致的 JS 执行暂停, 保持页面的流畅度.
  • 在运行期间, 长时间的脚本执行会阻塞主线程而导致页面没有响应. 将脚本的工作量分成多个小块来执行(使用 requestAnimationFrame() 或 requestIdleCallback() 进行任务调度)可以最小化响应性问题.
Progressive Bootstrapping(逐步引导)

因为优化交互性的成本比较高, 许多网站会考虑去优化内容的可见性. 当 JavaScript Bundles 很大时, 为了减少白屏时间(First paint time), 一些开发者会采用服务端渲染的方式, 当 JS 处理完成之后再将其 "升级" 为事件处理.

但这种方式也是有时间消耗的: 1) 通常会发送一个很大的 HTML 文件作为响应, 2) 在 JavaScript 完成处理之前, 页面可能只有一部分是可交互的.

因而逐步引导可能是一个更好的方式. 浏览请请求一个最小化的功能页面(仅由当前路由需要的 HTML/JS/CSS 组成), 当有更多资源请求时, 应用可以进行资源懒加载, 然后逐步解锁更多功能.

Loading code proportionate to what’s in view is the holy grail. PRPL and Progressive Bootstrapping are patterns that can help accomplish this.

参考
5人推荐
随时随地看视频
慕课网APP