在建立那些严重依赖于JavaScript网站的时候,有时我们会为自己发送的内容付出一些隐形的成本。在本篇文章中,我会介绍一些可以帮助你提升网站在移动设备上加载和运行速度的实用规则。
tl;dr:更少的代码 = 更少的解析/编译 (parse/compile)+ 更少的传送 + 更少的解压缩
网络
大多数开发人员考虑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%的压缩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是一种通过激进的(aggresive)代码分割和缓存来优化交互性的模式:
为了能将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的传输和解析/编译的时间消耗。
(考虑一下在我们所做的架构决策下,JS有多大的空间可以让我们的应用程序具有逻辑)
如果你正在建一个用于移动设备上的站点,请尽可能的在代表性硬件上开发,保持较低的JavaScript解析/编译的时间成本,并采用性能预算来确保团队对自身JavaScript的成本关注。