网页性能优化

网页性能优化

参考阮一峰老师的文章总结一下网页性能问题以及优化,包括css和js的方法。


网页生成过程

  1. HTML代码转化成DOM。
  2. CSS代码转化成CSSOM(CSS Object Model)。
  3. 结合DOM和CSSOM,生成一棵渲染树(包含每个节点的视觉信息)。
  4. 生成布局(layout),即将所有渲染树的所有节点进行平面合成。
  5. 将布局绘制(paint)在屏幕上。

其中影响性能的是4和5。这两步称之为“渲染”,在网页使用的过程中最少渲染一次,在用户访问网页的过程中还可能发生多次渲染。

那么,什么情况下网页会重新渲染呢?

  1. dom改变
  2. 样式改变
  3. 事件发生

以上任意一项发生改变,网页就会重拍和重绘。

重排必然导致重绘,而重绘不一定导致重排。


提高性能

于是乎,我们提高性能的思路就是:降低重排和重绘的频率和单次成本以减少出发浏览器对于网页的重新渲染(重排,重绘)。

  1. 尽量不要把读操作和写操作放在一个语句里。
1
2
3
4
5
6
7
8
9
10
11
// bad example
div.style.left = div.offsetLeft + 10 + "px";
div.style.top = div.offsetTop + 10 + "px";

// 拆开写

// good example
var left = div.offsetLeft;
var top = div.offsetTop;
div.style.left = left + 10 + "px";
div.style.top = top + 10 + "px";

一般来说:

  1. 样式表越简单,重新渲染就越快。
  2. DOM层级越高,成本越高。
  3. table渲染成本高于div。

技巧:

  1. DOM的多个读操作放一起写,不要读写操作穿插。

  2. 对于重排而言,缓存可以提高性能避免下次不必要的重排。

  3. 不要一次次改变样式,尽量一次性改变样式。

  4. 尽量使用离线DOM来改变样式

  5. 如果将一个元素display: none,再对该元素进行100次操作,最后恢复原本display。总计用了两次渲染。避免了在元素显示的情况下不必要的100次渲染。

  6. absolute fixed sticky渲染开销小,是由DOM决定的。

  7. 只在必要情况下将display设置为非none,这样可以极大减少渲染次数。对于visibility: hidden元素,只会重排一次,可能重绘多次。

  8. 可以使用虚拟DOM库。

  9. 使用 window.requestAnimationFrame()、window.requestIdleCallback() 这两个方法调节重新渲染。


刷新率

浏览器刷新率为60hz,当页面滚动时,浏览器会对网页以每秒60次的频率重新渲染。

也就是说,每隔$ 1000/60=16.7 $毫秒,浏览器就要渲染完毕一帧的画面。

解决办法是使用web worker,主线程用于ui渲染,worker线程用来渲染其他的任务。


network面板

打开chrome开发者工具的network面板。

点击原点开始录制,然后模仿用户操作,再点一下原点完成录制。

事件模式:查看影响性能的原因。蓝色表示载入,黄色表示js,则色表示渲染,绿色表示重绘图。

帧模式:用于查看每帧耗时,越高耗时越多。

两条线,上面一条表示30帧,低于这条线就能以高于30帧的频率渲染,下面是60帧线,低于它代表可以达到每秒60帧以上。


window.requestAnimationFrame()

使用一些js方法调节渲染提高性能。

这里 window.requestAnimationFrame() ,可以将一些代码放到下一次渲染时一起渲染。

这是一个让elements中的每一个元素高度增加一倍的方法 doubleHeight()

1
2
3
4
5
6
7
8
function doubleHeight(element) {
// read operation
var currentHeight = element.clientHeight;

// write operation
element.style.height = (currentHeight * 2) + 'px'
}
elements.forEach(doubleHeight);

这里浏览器对于该方法的渲染过程是:依此渲染每一个 elements 中元素的高度,并且每一个元素都要进行一次read和一次write。这就严重影响了性能。

使用 window.requestAnimationFrame() 将read和write分离,使得所有的write放到下一次执行。

1
2
3
4
5
6
7
8
9
function doubleHeight(element) {
// read
var currentHeight = element.clientHeight;
// 使用window.requestAnimationFrame()
window.requestAnimationFrame(function() {
element.style.height = (currentHeight * 2) + 'px';
});
}
elements.forEach(doubleHeight);

这样浏览器会一次性读取每一个elements中元素的高度,并在浏览器下一次渲染时一次性将所有的elements中的元素高度写为原来的两倍。

window.requestAnimationFrame() 的应用场景: scroll 事件监听函数以及 animation


window.requestIdleCallback()

这个函数指定当只有一帧的末尾有空闲时间才会执行毁掉函数。

1
requestIdleCallback(fn, 1000);

上面代码中,只有当前帧的运行时间小于16.66ms时,函数fn才会执行。否则,就推迟到下一帧,如果下一帧也没有空闲时间,就推迟到下下一帧,以此类推。

1000表示指定毫秒数,作用是如果1000ms内每一帧都没有空闲时间,就强制执行 fn ,也就是一个deadline的作用。

函数 fn 可以接受一个 deadline 对象作为参数。

1
2
3
4
5
6
7
8
9
requestIdleCallback(function someHeavyComputation(deadline) {
while (deadline.timeRemaining() > 0) {
doWorkIfNeeded();
}

if (thereIsMoreWorkToDo) {
requestIdleCallback(someHeavyComputation);
}
});

上面代码中,回调函数 someHeavyComputation 的参数是一个 deadline 对象。

deadline对象有一个方法和一个属性: timeRemaining()didTimeout

  1. timeRemaining() 方法

timeRemaining() 方法返回当前帧还剩余的毫秒。这个方法只能读,不能写,而且会动态更新。因此可以不断检查这个属性,如果还有剩余时间的话,就不断执行某些任务。一旦这个属性等于0,就把任务分配到下一轮 requestIdleCallback

前面的示例代码之中,只要当前帧还有空闲时间,就不断调用doWorkIfNeeded方法。一旦没有空闲时间,但是任务还没有全执行,就分配到下一轮 requestIdleCallback

  1. didTimeout属性

deadline对象的 didTimeout 属性会返回一个布尔值,表示指定的时间是否过期。这意味着,如果回调函数由于指定时间过期而触发,那么你会得到两个结果。

  • timeRemaining方法返回0
  • didTimeout 属性等于 true

因此,如果回调函数执行了,无非是两种原因:当前帧有空闲时间,或者指定时间到了。

1
2
3
4
5
6
7
8
9
function myNonEssentialWork(deadline) {
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && tasks.length > 0)
doWorkIfNeeded();

if (tasks.length > 0)
requestIdleCallback(myNonEssentialWork);
}

requestIdleCallback(myNonEssentialWork, 5000);

上面代码确保了,doWorkIfNeeded 函数一定会在将来某个比较空闲的时间(或者在指定时间过期后)得到反复执行。

requestIdleCallback 是一个很新的函数,刚刚引入标准,目前只有Chrome支持,不过其他浏览器可以用垫片库。


网络性能优化

包括了静态文件做cdn,压缩代码,以及压缩图片等等。

评论