前端性能之JavaScript成本(2018)

1,365 阅读13分钟

关于原文

原文是在Medium上面看到的,Chrome工程师Addy Osmani发布的一篇文章,这位的Medium上面的自我介绍里面有一句Passionate about making the web fast,和这篇文章的主体可以说非常契合了。

最近在做一个服务端渲染的项目,到底页面性能的提升能够带来多少的意义,或者到底有多少合理的方法来让精雕细琢移动端的用户体验。

这篇文章主要是部分内容的翻译,并且结合自己的一些想法,以备在项目架构的时候考虑到这些相关的东西。

原文内容会按照引用样式排版

原文-The Cost Of JavaScript 2018

这篇文章很干,干到一张图都不存在,所以想读下去的请做好准备吧~,更推荐直接去读原文哦!!

前言

首先,JavaScript仍旧是我们发送到用户移动设备上的最昂贵的资源,因为它可以在很大程度上延迟用户的交互。

JavaScript阻塞页面上的交互动作,所有同步执行的JavaScript代码不会中断自己的执行来给突然触发的交互事件让路,并且如果在交互的时候需要页面样式的变化,也是会被阻塞的。React 16中新加入的Fiber功能,就是为了将同步的re-render操作分片,来让页面的交互可以间歇进行而不是完全阻塞。关于React Fiber的原理,可以看这一篇:React 16.0 Fiber源码解读

React在Release Note中也写到了,React 16还有一大进步就是相比起15.6版本来说,其react库压缩到了5.3kb,gzip压缩后可以达到2.2kb,而react-dom库也从141kb压缩到了103.7kb,库大小的减少也能够很好地提升React在客户端的执行速度,在页面首次渲染的时候加载更少的资源。

  • 为了保证速度,仅仅加载当前页面必须的JavaScript;
  • 提前做好性能预算,并且合理利用;
  • 做好JavaScript打包以及代码审计工作;
  • 每一个交互都是从一个新的“可交互时间”开始的,考虑如何在这种情况下进行优化;
  • 如果一段客户端JavaScript并不能够提升用户体验,那么就要问问你自己这段代码是否是必须的。

原文的文章很长,所以放了一个tl;dr:在文章最前面,文章的五个重点都列出来了,如何压榨每一个Byte的JavaScript的性能,需要从多个方面考虑可精简的JavaScript。

web由于用户“体验”而膨胀

也许你根本就不知道自己页面中的JavaScript到底占据了多少物理资源来执行,尤其是在移动设备上。目前的现代网页平均会使用250KB的压缩JavaScript,如果没有压缩的话,大概是1MB左右的脚本,这些脚本都需要浏览器来执行,无论是在移动设备还是PC上面。

在公司网络的环境下,使用lighthouse来对网易云音乐的首页进行检测,从开始HTTP请求一直到页面开始可以交互的时间大概在3s左右,根据5s原则来说,已经是一个很好的体验了,很多元素的延迟加载起到了很好的作用。如果你想看到自己的网页性能到底如何,可以使用lighthouse快速生成一个网页性能的检测报告。

移动端用户体验随着JavaScript阻塞交互事件超过14秒以上,逐渐消失。

导致移动端上面代码阻塞时间的主要原因是移动端CPU的性能以及网络状况。

这里显示的中国并不是4G覆盖率非常低,而是没有数据,但是可以看到西欧、北美等地区的4G覆盖率也只是60%~80%之间,并不能够达到基本全覆盖,所以为了这部分3G用户的用户体验,缩减JavaScript压缩后文件的大小也是必然的。

并且移动端设备的性能也是瓶颈之一,智能手机的普及率虽然比较高,但是质量参差不齐,许多移动端设备还停留在1G甚至512RAM的情况下。

大部分互联网公司都会采用两套web来实现移动端和桌面端,移动端采用高压缩的页面,来减少网络时间和加载时间。

JavaScript存在成本

如果页面有着过多的脚本,那么就需要考虑code-spliting来分开bundle代码,或者通过tree-shaking来减少JavaScript的包袱。

目前我们的业务项目采用React+Node.js的SSR来进行SEO优化和首屏性能提升。我们的JS Bundle中有着很多的JavaScript库代码:

  • react&& react-dom等客户端框架;
  • 大型SPA可选的状态管理解决方案:mobxvuexreduxrxjs
  • ES6、ES7等polyfills,为浏览器厂商还债;
  • @music等组件库,包括Utils组件以及UI组件。

即使已经完成了code split,首屏加载中,上面的这些库也会有一大部分被加载进来,造成整个项目的JavaScript冗余。

整个页面在加载的时候,有着几个重要的时间节点。

是否开始有内容显示在页面上了。也就是用户能够感受到自己得到了响应;

是否有完整的内容显示在页面上,也就是可交互的内容已经显示了出来;

是否可以开始进行交互了,也就是意味着用户能够开始对页面进行正常操作。

前两个阶段在服务端渲染的情况下,大部分进行的还是页面的render操作,render是没有太多办法来对其进行加速的。所以为了提升交互效果,第三阶段是必须着力解决的。不能够产生交互的主要原因是脚本还没有加载完成,导致了页面阻塞。加速加载和执行,通过减少JavaScript包的大小是可行的方法。

另外一种方法是通过SSR,为用户提供更快的首屏渲染速度,并且在之后,将JavaScript注入到页面当中。

通过<script>标签等主进程加载过多的JavaScript会造成阻塞问题,而采用Web Worker进程或者缓存来进行页面脚本的执行能够得到更短的阻塞时间。

我们估测了Google News的移动端可交互时间,在高端设备上大约是7秒左右,而低端设备则达到了55秒。

中低端设备的JavaScript运行速度远远比我预期的要长很多,由于生活和工作环境中较少接触这类设备,所以这类用户设备的比例是需要进行埋点获取的,如果这类设备的比例较高(在国内是比较有可能发生的),那么就需要为这类用户进行JavaScript的削减或者压缩。

Pinterest将打包后的JavaScript从2.5MB压缩到了小于200KB,响应时间从23秒降低到了5.6秒,这样带来的直接结果就是,他们的收入提升了44%而注册量提升了753%Orz,移动端的周活提升了103%。

我们需要做的重点是防止JavaScript成为整个网站的瓶颈。

需要记住的是,如果想要让JavaScript变得更快,那么需要做到下面几点:

  • 下载快
  • 解析快
  • 编译快
  • 执行快

Parse/Compile

上面是各大网站在V8引擎上面的脚本执行时间图,解析和编译阶段占了总时间的大约10%~30%,在Chrome 66中,V8可以在后台线程编译代码,可以将编译时间减低到大约20%左右,但是也很少见到能够在50ms之内编译完的JavaScript代码。

另一个要注意的事情就是,JavaScript的大小并不完全意味着它的时间消耗,一个200KB的图片和一个200KB的JavaScript所占用的时间是完全不同的。

两者的下载时间应该是差不多并且和大小强相关的,但是图片需要解码,栅格化以及绘制到屏幕上,而JavaScript代码包需要解析,编译以及执行。JavaScript的时间消耗基本是要大于图片的,这个差距也取决于设备的CPU性能。

根据上述内容,**在进行性能测试的时候,尽量让环境恶劣起来,不要使用高速的网络环境以及高性能设备来进行测试。**因为用户设备和环境的均值可能是你想象不到的。

可变性会杀死用户体验,高性能设备可能会变慢,高速网络也可能会变慢,可变性最终会让所有事情都变慢。

可变性需要让开发人员降低开发时的基准线,来保证每一个用户的体验。如果你的团队能够通过一些策略来获知所有访问你的站点的用户环境,那么可以很方便地对于站点的性能兼容性进行测试。在测试的时候,采用用户中具有代表性的网络和设备环境来进行测试。

It's important to know your audience.

  • 对于网络,低端网络环境需要更小的JavaScript bundle。这就要求减少代码的冗余、缩小代码体积、并且进行压缩;
  • 对于设备,做好对于重复访问数据的缓存工作,解析时间对于低端设备来说是最重要的。

当我们的站点越来越依赖于JavaScript的时候,我们有时候就会为了不容易看见的发送到客户端的代码付出代价。

如何发送更少的JavaScript

这一点的关键在于如何发送最少限度的JavaScript到客户端,并且能够保证用户的正常体验。Code-splitting是其中的重点。

目前来说,Code-splitting常用的方法就是在bundle代码的时候,仅仅返回当前路由对应的相关代码,而不返回整个庞大的代码包。对于路由的切分以及库的引入来说,这一个原则至关重要。无论什么理由,都尽量不要将其他路由的代码注入到当前访问的路由当中。路由的懒加载是decrease你的JavaScript加载速度的重点。

import Loadable from 'react-loadable';
const LoadableOtherComponent = Loadable({
    loader: () => import('./OtherComponent'),
    loading: () => <div>Loading...</div>
});
const MyComponent = () => {
    <LoadableOtherComponent />
};

在React中添加code-splitting可以通过React Loadable进行,这个库是一个HOC,可以动态加载需要的React组件,而不会在首次渲染的时候就将所有的页面组件都加载进来,即使它暂时不会被使用。

现在也有很多库可以帮助你来定位自己的代码包,来帮助你从代码层面减少JavaScript代码的长度。比如webpack bundle analyzersource map explorerbundle buddy。这些工具会审视你的代码,并且找到其中的冗余,大型库以及一些未使用的依赖。

打包审查可以着重于一些大型依赖,或者是给你一个较轻的低位替代。

措施,优化,监控以及重复

如果你不确定自己的工程代码是否存在这些问题,通过LightHouse可以审查你的站点。

cnpm install -g lighthouse
lighthouse http://yoursites.com

快速生成一份站点的性能审查报告。

云音乐主站的审查报告大概是这样的,我们的移动端主站大概需要3秒左右的时间能够得到交互响应。

由于这篇文章是从我的桌面上淘出来的,所以图似乎都挂掉了。。有兴趣的可以自己去跑一下云音乐主站的代码哦!

Code Coverage是一个用来发现你的页面中的未使用JavaScript以及CSS的DevTools。使用这个工具可以看到页面中有多少代码影响了加载性能,并且这些代码让你付出多少时间的代价。

这是主站测试环境下的代码覆盖率,可以发现libs文件基本上有一半都未在使用。而CSS更加夸张,95%的代码都没有使用过。

PRPL原则

PRPL(Push、Render、Precache、Lazy-Load)模式适用于尽力将每一个单独路由的代码拆开,然后利用service worker来pre-cacheJavaScript代码,这些懒加载的代码是与当前路由强相关的路由的逻辑代码。

也就是说,我们在进行路由加载的时候,仅仅加载一个纯净的路由页面。当这个路由页面渲染完毕之后,我们通过一个路由相关的list来将其他与当前路由有跳转规则的路由页面加载进来,通过service worker来在后台线程进行懒加载与解析。并且根据我们当前的环境,来进行优雅降级,如果设备不支持后台线程,那么就采用主线程来进行加载。

性能预算

性能预算是保证所有开发人员在一个频道的关键,性能预算定义了一个常量,来让团队有着共同的性能目标。

性能预算一般包括下面几个部分:

  • Milestone timings(里程碑时间?):这个时间一般基于加载页面时候的用户体验,比如可以开始交互的时候。这个时间一般是页面完成加载的精确时间。
  • Quality-based metrics:基于纯粹的值,比如JavaScript的大小,HTTP请求的数量,这个值主要关注浏览器体验。
  • Rule-based metrics:通过LightHouse或者其他页面测试工具得到的评分。

由于现在的大型项目总都是由多个开发人员一起进行开发的,这些开发人员对于页面的性能并没有一个统一的标准,通过上面三个标准,每个人在加入了自己的代码之后就可以对于性能进行测试,来确认自己是否影响到了整个项目的性能预算。比如自己的库导致了团队的JavaScript代码超量。这时,团队的人就需要根据情况来削减自己的代码量。

下面的有点可怕,就放原文了:

Here’s an action plan for performance:

Create your performance vision. This is a one-page agreement on what business stakeholders and developers consider “good performance”

Set your performance budgets. Extract key performance indicators (KPIs) from the vision and set realistic, measurable targets from them. e.g. “Load and get interactive in 5s”. Size budgets can fall out of this. e.g “Keep JS < 170KB minified/compressed”

Create regular reports on KPIs. This can be a regular report sent out to the business highlighting progress and success.

在GIT合并的时候设置LightHouse的检测规则,如果导致了LightHouse评分降低,则blocked该次合并。在对于性能极致要求的业务情况下,这个方法的确能够有效地提升业务代码的质量。

Get fast, Keep fast

性能不是一蹴而就的,许多细小的变动都可能会带来大的收益。