网页的生成过程
HTML
代码转化为DOM
CSS
代码转化成CSSOM
(CSS Object Model
)- 结合
DOM
和CSSOM
,生成一颗渲染树 (包含每个节点的视觉信息) - 生成布局 (
layout
),即将所有渲染树的所有节点进行平面合成 - 将布局绘制(
paint
)在屏幕上
最为耗时的在第四步和第五步
生成布局(flow
)和绘制(paint
),合称为渲染(render
)
重排和重绘
网页生成的时候,至少会渲染一次。用户访问的过程中,还会不断重新渲染。
修改DOM
、修改样式表、用户事件(鼠标悬停,页面滚动、输入框输入文字、改变窗口大小等)以上三种操作都会导致网页重新渲染。
重新渲染,就需要重新生成布局和重新绘制。前者叫做重排(reflow
),后者叫做重绘(repaint
)。重绘不一定需要重排,比如改变某个元素的颜色,就只会触发重绘, 因为布局没有变化,但是重排必然会导致重绘,比如改变某个元素的位置,就会同时触发重排和重绘,因为布局改变了。
提高网页性能,就是要降低重排和重绘的频率和成本,尽量少触发重新渲染
目前,浏览器已经很智能了,会尽量把所有变动集中在一起,排成一个队列,然后一次性执行,尽量避免多次重新渲染。
但是样式的写操作之后,有下面这些属性的读操作,会引发浏览器立即重新渲染
offsetTop/offsetLeft/offsetWidth/offsetHeight
scrollTop/scrollLeft/scrollWidth/scrollHeight
clientTop/clientLeft/clientWidth/clientHeight
getComputedStyle()
渲染的一般规则
- 样式表越简单,重排和重绘就越快
- 重排和重绘的
DOM
元素层级越高,成本就越高 table
元素的重排和重绘,要高于div
元素
提高性能的九个技巧
DOM
的多个读操作,或多个写操作,应该放在一起。不要两个读操作之间,加入一个写操作。- 如果某个样式是通过重排得到的,那么最好缓存结果,避免下次用到的时候,浏览器又要重排。
- 不要一条条地改变样式,而要通过
class
或者csstext
属性,一次性地改变样式 - 尽量使用离线
DOM
,而不是真实的网面DOM
,来改变元素样式。比如,操作Document Fragment
对象,完成后再把这个对象加入DOM
。再比如,使用cloneNode()
方法,在克隆的节点上进行操作,然后再用克隆的节点替换原始节点。 - 先将元素设为
display: none
(需要1次重排和重绘),然后对这个节点进行100次操作,最后再恢复显示(需要1次重排和重绘)。这样一来,你就用两次重新渲染,取代了可能高达100次的重新渲染。 position
属性为absolute或fixed
的元素,重排的开销会比较小,因为不用考虑它对其他元素的影响- 只在必要的时候,才将元素的
display
属性为可见,因为不可见的元素不影响重排和重绘。另外,visibility : hidden
的元素只对重绘有影响,不影响重排。 - 使用虚拟
DOM
的脚本库,比如React
等。 - 使用
window.requestAnimationFrame()
、window.requestIdleCallback()
这两个方法调节重新渲染
刷新率
很多时候,密集的重新渲染是无法避免的,比如scroll
事件的回调函数和网页动画。网页动画的每一帧都是一次重新渲染,每秒低于24
帧的动画,人眼就能感受到停顿,一般的网页动画,需要达到30
帧和60
帧,才能比较流程,如果达到每秒 70
帧以上,就会极其流畅,60
帧意味着 一秒之内进行60
次重绘,每次重新渲染的时间不能超过16.66
毫秒。
一秒之间能够完成多少次重新渲染,这个指标就被成为刷新率FPS
,如果想达到60
帧的刷新率,就意味着JavaScript
线程每个任务的耗时,必须少于16
毫秒。一个解决办法是使用Web Worker
,主线程只用于UI
渲染,然后跟UI
渲染不相干的任务,都放在Worker
线程。
Chrome浏览器开发者工具的Timeline
面板
Timeline
面板提供两种查看方式:横条的是”事件模式”(Event Mode
),显示重新渲染的各种事件所耗费的时间;竖条的是”帧模式”(Frame Mode),显示每一帧的时间耗费在哪里。
网页性能优化方案
使用 window.requestAnimationFrame()
,它可以将某些代码放到下一次重新渲染时执行,让读操作和写操作分离,把所有的写操作放到下一次重新渲染。
使用 window.requestIdleCallback()
,它指定只有当一帧的末尾有空闲时间,才会执行回调函数。
window.requestIdleCallback(()=>{})
只有当前帧的运行时间小于16.66ms
时,函数才会执行。否则,就推迟到下一帧,如果下一帧也没有空闲时间,就推迟到下下一帧,以此类推。
window.requestIdleCallback(()=>{},5000)
它还可以接受第二个参数,表示指定的毫秒数。如果在指定 的这段时间之内,每一帧都没有空闲时间,那么函数将会强制执行。