原文链接:https://medium.com/dev-channel/the-cost-of-javascript-84009f51e99e
当我们建立的网站越来越多依赖Javascript时,我们有时候会为了一些我们不容易看到的地方付出代价。在这边文章中,我会讲述一些规范,如果你想让你的网站在移动设备上更快加载和响应的话,这些规范可能会对你有帮助。
tl;dr: less code = less parse/compile + less transfer + less to decompress
tl;dr是too long; don't read的缩写
网络
当大多数开发者想到Javascript的开销时,可能会联想到下载和执行开销。传送的数据量越大,那用户连接的速度就越缓慢。
即使在一些大国家,如果用户的有效网络不是3G, 4G 或者WiFi的话(用户可能在咖啡厅用着wifi,但是实际网速是2G),那么这就会成为一个问题。
你可以通过以下几种方式来减少网络传输的开销:
- 只传输用户需要的. 代码分离(Code-spliting)能起到一定作用.
- 混淆(丑化) (ES5有Uglify, ES2015有babel-minify 或者 uglify-es)
- 强力压缩 (使用 Brotli ~q11, Zopfli 或者 gzip). 相较于gzip,Brotli 在压缩比上更胜一筹. 它使CertSimple在JS的体积上节省了17%,使LinkedIn在加载时间上节省了4%.
- 删除无用代码. 与DevTools code coverage类似. 对于剥离的代码, 见 tree-shaking, Closure Compiler的高级优化和其它类似的插件,像 lodash-babel-plugin 或者 Webpack的 ContextReplacementPlugin. 使用babel-preset-env 和 browserlist 以避免转换已经存在于现代浏览器的一些特性. 资深开发者可能已经发现 analysis of their Webpack bundles 可以帮助去除那些不必要的依赖.
- 缓存 优化脚本有效时间以及ETag来避免传输没有变化的数据. Service Worker缓存能帮助实现弹性网络,并且使你更早使用一些特性,像V8’s code cache. 同时也可以通过filename hashing学习持久化缓存.
解析/编译
一旦脚本下载完成了之后,JS最大的开销之一就是JS引擎的解析/编译代码。在Chrome开发者工具里的“性能”模块,解析和编译代码用黄色标识.
通过Bottom-Up/Call Tree(调用树),可以看实际的解析/编译用时:
但是,为什么这个很重要呢?
花费大量时间在解析/编译代码上会延迟用户与网站的交互,破坏用户体验。在网站呈现之前,JS量越大,花在解析/编译上的时间就越长。
对于大小相同的JS和图片或者网页字体,JS需要浏览器花费的时间最多— Tom Dale
与Javascript相比,处理相同大小的图片所需要的开销明显小很多。
当我们讨论解析和编译的速度之慢时,上下文是很重要的。我们的讨论是基于平均水平的移动设备。平均水平的用户使用的移动设备可能是CPU/GPU很慢的、没有L2/L3缓存的或者甚至内存很有限的。
网络和设备不总是匹配的。一个用户可能有很好的网络条件,但是只有一部烂手机。相反,一个用户可能有一部神机,却碰上了龟速网络 — Kristofer Baxter, LinkedIn
在JavaScript Start-up Performance文中, 我提到了分别在低端和高端机型中1MB原始JS代码的解析时间. 它们之间的差距达到了2-5倍.
那实际的网站如何呢,比如CNN.com?
在高端机iPhone 8上,解析/编译JS代码只需大约4s,而在平均水平的手机Moto G4上却要花上将近13s。这很明显得影响了用户能够多快看到界面。
这就要求我们要更加注重一些平均水平的设备的测试,而不仅仅是自己口袋里的高端机。上下文是很重要的:一定要针对你的用户的设备和网络进行优化。
可以通过 mobile device classes 来看看真实用户的分析情况。
我们真的是传输了太多的JS代码吗? 呃。。有可能 :)
使用HTTP Archive (top ~500K sites)来分析JS在移动设备上的使用情况JavaScript on mobile, 我们就能发现50%左右的网站需要14s以上才能真正让用户用上。这些网站光花在解析/编译JS上的时间就多达4s.
介于以上这些情况,怪不得用户在还没有看到页面之前就离开了。我们当然可以在这点上做得更好。
删除一些非必要JS代码能有效减少转换时间、CPU的解析/编译时间以及内存占用。同样也能是用户更快得与网站交互。
执行时间
当然,编译和解析只是JS开销的一部分。执行JS代码也是主线程上必须要做的,如果执行时间冗长,也会直接影响用户体验。
一旦脚本执行时间超过50ms,后果不堪设想 — Alex Russell
为了减少执行时间,你可以将JS代码分离成一块块的,以免阻塞主线程。
设计模式
有时候一些设计模式能够帮助你,比如基于路由的代码分块 (route-based chunking) 或者PRPL.
如下图所示,PRPL就是一个利用代码分离和缓存方式来优化交互体验的模式:
让我们来看看这个影响.
我们使用V8的 Runtime Call Stats 分析了一些主流网站以及PWA的加载时间。可以看到,解析时间在整个加载时间中占了可观的部分:
Wego,是使用了PRPL的一个站点,让每个路由都保持很少的解析时间,使得用户能更快得与网站交互。上图中的很多网站也是采用了代码分离和性能预算 (performance budget) 来尝试降低JS开销。
其它开销
JS也会在其它方面影响页面性能:
内存。页面可能会因为垃圾回收而导致频繁的闪烁或暂停。当浏览器回收内存的时候,JS执行就会暂停。这就导致了当浏览器频繁回收垃圾时,JS执行的暂停频率就会比我们想象中的更多。避免内存泄漏和频繁的垃圾回收能够是页面更稳定。
在运行时,如果JS运行时间过长就会阻塞主进程,导致页面无法交互。把这些任务分成一小块一小块 (可以采用 requestAnimationFrame() 或者 requestIdleCallback() 或者 scheduling) 能够最小化其带来的影响。
渐进式 Bootstrapping
为了让网站更快呈现在用户面前,许多网站会使用服务器端渲染来实现,然后在页面返回后通过绑定事件来“升级”它。
当心 -- 这种方式也有它的开销。一方面传输回来的HTML比较大,另一方面用户必须等到JS处理完毕才能真正与页面进行交互。
渐进式 Bootstrapping 可能是一种更好的方法。先发送一部分功能性页面(只是当前路由需要的HTML/JS/CSS)回来。当更多的资源传输回来时,页面就会进行懒加载并且解锁更多的功能。
加载当前页面的代码实在是非常好的方法。PRPL和渐进式 Bootstrapping 就是能帮助实现这点的模式。
结论
在网络不佳的情况下,传输数据的大小是至关重要的。对于CPU不给力的设备,解析时间是很重要的。
参考 Alex Russell 的 “Can You Afford It?: Real-world Web Performance Budgets”。
如果你正在搭建一个基于移动设备的站点,尽量在典型的设备上开发。减少JS解析/编译时间,采用Performance Budget让团队成员都能检测JS开销。
硬广
这是本人的前端技术小程序,基本上所有的文章都会同步更新在小程序中。欢迎大家来凑热闹。