引言
前端页面的性能一直都是一个热议的话题,从老早的“军规”开始我们就一直在关注页面的性能问题。
在前面巨人们的身后每个人也有自己的一些页面性能的经验,本文主要是介绍如何评估页面加载完成之后的性能。
浏览器如何渲染一个页面
先附上一张图片:
这是在网上找的一张图,虽然是用来描述 performance 的 API 但是也很好的描述了浏览器是怎么处理一个页面渲染的。
这是我们在 PC 时代考虑的浏览器性能,主要在服务端响应、文档下载、文档渲染三个阶段,性能优化大部分也集中在这三个阶段。针对这部分的监控、分析非常的普遍了,在JSTracker 上也有这个数据的分析。
为什么刚才说 PC 时代呢,除了这些指标作为前端还有需要关注的什么性能呢?
页面加载之后的操作体验!(注:这里的加载之后近似在 onLoad 之后)
页面操作的流畅度
在 PC 的 CPU 越来越牛逼、内存越来越大的时候,前端的代码复杂度也在上升。
以往我们都默认认为只要资源加载好了、只要资源加载快了那么我们的页面性能就是棒棒哒的,现在不再是这样。
这里涉及到两个问题:
- PC 端的 CPU 性能、内存的性能虽然很高了,但是前端代码也更复杂了;
- 除了 PC,还有无线端;
于是我们的性能指标多了一项:页面流畅度。
页面流畅度如何感知
卡顿的感受会在很多地方出现,比如:
- 你在逛某个页面的时候愉快的的滚动鼠标滚轮,但是页面会突然的顿一下。
- 你在打开手机上直接访问 PC 页面(或者 H5 页面)的时候,发现浏览器就像死掉了一样。
拿两个 PC 页面来感受一下卡顿:
页面 1:http://codepen.io/taobaofed/pen/jbeQbN
页面 2:http://codepen.io/taobaofed/pen/JYmeYJ
很明显页面 1 比页面 2 卡顿很多。
卡顿就是这样的感觉,但是怎么来衡量这个卡顿的程度呢?
总不能让用户直接说吧,就像上面的两个页面:有人开心的时候会说都很流畅,要是不开心的时候他会说两个都有点卡顿。这样的评价太主观了。
如何客观的度量页面卡顿程度
首先要知道页面为什么会卡顿呢?
我们还是以上面两个页面做比较,看看浏览器里面的 Timeline;
页面 1:
页面 2:
这两个页面的渲染 fps 可以看出来,的确第一个页面很卡。一般我们要求页面要达到 60 fps( 60 帧/秒)。上面说到页面 2 其实也是有稍微的卡顿的,只不过比页面 1 好很多。
如果按照 60 fps 计算,那么每一帧执行时间为 1/60s,也就是 16.7ms。
为什么页面会看起来比较卡
浏览器的渲染是单线程的,在某一个时刻要么在进行 JS 运算,要么在进行 UI 渲染。不会同时进行。
如果我们的脚本在改变 UI 那么这个脚本的执行时间不要超过了 16.7ms,否则页面在这个周期中无法进行 UI 变化,那么看起来就是跳帧(卡顿)了。
如果你的页面没有改变 UI ,情况会好很多,浏览器看起来就是失去了响应,Hover 滚动都动不了了。
准备了一个简单的代码,用来说明如果脚本执行超过 16.7ms 会有什么感觉:
页面 3:http://codepen.io/taobaofed/pen/YyJJzW
在这里例子中两个按钮的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//按钮1 $('#do').on('click', function(){ $('#status').text('calculating....'); setTimeout(long,0); }); //按钮2 $('#do2').on('click', function(){ $('#status').text('calculating....'); long(); }); |
点击第一个按钮,下方文字会立即变为“calculating….” 点击第二个按钮,下方的文字跳过了变为“calculating….”阶段,直接变为“calclation done”。而且按钮的 Active 状态都不会消失,直到脚本执行完成。 为什么会出现这样的结果呢? 这个就是浏览器的单进程模式确定的,setTimeout 把要执行的任务扔到浏览器的单线程的队列尾部,线程能够有机会渲染 UI !(图片来自参考文章 3) 第一个按钮点击之后 UI 渲染能够马上的得到运行时间,第二个按钮点击之后 UI 渲染必须要等到 long 方法执行完毕,所以看起来像是浏览器卡死了。
如何采集页面卡顿的程度
目前为止,我们已经知道了什么是卡顿、卡顿的发生原因、如何在 Chrome 中查看卡顿,接下来我们要想办法用 JS 获取页面的卡顿程度。 利用上述的原理:浏览器是单线程的,如果卡顿发生了那么后面队列堆积的方法就得不到执行。 假如我们配置一个定时器,每隔一段时间 t 就向浏览器的线程队列中丢一个方法进去:
- 如果线程队列是空闲的,那么我们理论上可以检查到我们的方式每次都是准时的间隔 t 被调用一次;
- 如果线程队列是繁忙的,那么这个间隔时间将是大于 t 的;
试验方案有了,接下来进行试验。
1 2 3 4 5 |
var t = new Date(); setInterval(function(){ console.log(new Date() - t); t = new Date(); }, 100); |
还是刚才的页面 3, 请打开控制台看看:
当点击按钮的时候控制台的 console 会停止,隔一会儿再输出数据,而且的确如我们预料的间隔时间变长了。
有兴趣的同学可以打开页面 1、页面 2 的控制台也可以看到数据。
接下来的事情是把这些数据收集起来,形成一个指标。这事情就好办多了:
- 统计页面一段时间的这些 t 值的和,可以计算出页面的拥堵程度,这个值理论上是和 CPU 的时间消耗成正比的,所以我们定义这个值为 CPU 消耗;
- 统计这些 t 值在什么时候开始归于平静,这个值也就是浏览器线程开始闲下来的时间,我们定义这个值为页面可操作时间;
- 统计一段时间这些 t 值超过某个阈值的次数,比如设置的间隔是 100ms,t 值过了 200 的次数和总次数的比。我们定义这个值为页面渲染的 CPU 占比;
数据可视化
一些浏览器是不能直接看到 console 的,比如手机浏览器。于是我们提供一个小插件(bookmarklet)。
页面 4:http://codepen.io/taobaofed/pen/xwyQwd
然后打开任意页面可以看到这个:
数据统计
这个数据目前已经纳入到到 JSTracker 的性能数据中。
Taobao 首页的数据:
如何优化
原则是每段连续执行的 JS 都尽量在 16.7ms 内完成,主要可以按以下方案进行:
减少会引起页面重绘(redraw)的方法的调用
这些值的列表如下(不完整,如果你不确定可以写个循环测试一下):
clientHeight, clientLeft, clientTop, clientWidth, focus(), getBoundingClientRect(), getClientRects(), innerText, offsetHeight, offsetLeft, offsetParent, offsetTop, offsetWidth, outerText, scrollByLines(), scrollByPages(), scrollHeight, scrollIntoView(), scrollIntoViewIfNeeded(), scrollLeft, scrollTop, scrollWidth
如果要使用尽量吧这些值缓存起来,不要再循环中直接调用。(有兴趣的同学可以对比一下页面 1 和页面 2 的区别,其实只有一行)。
除了缓存还可以像页面 2 一样,直接不去获取,因为这些值可能我们是能够预测的,不要要再让浏览器计算。
将某些耗时的操作放到空闲的时候再去做(requestAnimationFrame、setTimout)
如页面 3 让 UI 渲染能够有 CPU 时间执行。
但是如果你的这个耗时计算耗时太长了你可以考虑是否能将一些不是立即需要的任务分拆掉,平均的分配到各个帧。
涉及到页面的动画元素能用 GPU 最佳
我们可以给某些元素加上:
1 |
-webkit-transform: translateZ(0); |
强制让浏览器用 GPU 渲染这个层,不过这样做要适量,多了也容易出问题。(参考:http://wesleyhales.com/blog/2013/10/26/Jank-Busting-Apples-Home-Page/ )
釜底抽薪:降级
你可以通过某种方式检测到浏览器太卡了,那么降级之……
这个监控对页面本身性能的影响
由于页面被注入了这个一个定时器,可能会对页面造成影响的,虽然这个影响非常低但是还是必须要考虑:
第一层,强力优化这部分代码的性能:
在定时器中执行的任务都是纯 JS 运算,我们统计过这个部分的代码的消耗平均不到 0.04ms,占一帧时间的 0.23%。
第二层,抽样少量的数据进行数据采集,目前采样 1% 的。