每天都在敲代码画页面,那页面是怎么渲染出来的呢?
在熟悉html、css、js之后,我们基本就能把大部分页面效果做出来,无非是粗糙还是精细的问题。但是我们写的代码是怎么渲染成页面的呢?
先用直觉感受一下,如果我们需要在纸上绘制一张明确的图,那我们应该确定纸上有什么元素,然后各个元素应该出现在什么位置,确定完后再是我们去绘制这么一张图。
那么浏览器也是一样的
浏览器一开始是请求资源,在拿到资源过后开始进行分析,通过解析html和css分别得到dom树和css树,再将二者组合在一起组成render树交给浏览器完成绘制,绘制完成后才会呈现页面。
这么一来页面渲染的情况就与render树的构建情况息息相关。仅凭直觉来看,要想提高渲染效率,一方面要尽可能少的去修改render树,另一方面就是要尽可能小的去修改render树。
那么render树里有什么呢?
首先我们要知道浏览器是使用流式布局模型,(什么是流式布局?)那么要渲染页面需要知道页面里各个元素的大小、位置和效果,render树里就包含了从html和css里解析出来的存在且与显示相关的元素信息。
为什么说是存在呢?因为像是display: none的元素他实际上是不存在于布局中的,那么它就不会体现在render树里。
为什么说是与显示相关的呢?因为link、script这些标签关联的是一些资源或者脚本,不直接与当前渲染相关联,那么它们也不会体现在render树里。
那么得到render树后发生了什么?
首先浏览器会根据render树里的信息得到页面元素的布局信息(layout),比如大小啊、位置啊,获取的这个过程叫过回流。
在触发回流之后,页面的布局信息被加载了,那么相应的就要加载其效果,这时候就把color、box-shadow等设定好的样式绘制到元素上。
等全部工作完成之后,页面就完成加载然后显示出来。
所以,第一次加载页面的时候肯定是会有回流与绘制操作的。
那么第一次加载完成后呢?
根据上面的流程,如果有修改render树的操作,就会对页面做更新,那我们来看看页面是怎么更新的。
重排
我们已经知道页面是先有布局再有效果,那么如果我们对渲染树的布局做了修改,那么页面就会先对元素重新排列,然后再在对应的位置更新元素效果。
所以如果我们修改了元素的位置信息,那么就会先触发回流,然后再触发绘制。修改布局的这个过程就叫做重排。第二次回流,重新排列,很合理嘛。
重绘
如果我们没有修改布局,仅仅是修改了效果,那么仅仅只会影响绘制阶段,那么浏览器会根据新的效果重新绘制,这个过程就是重绘。
那么什么情况下会重排?什么情况下会重绘呢?
直觉上来说修改节点的位置或者效果的效果会导致重排或者重绘,但是实际上不仅仅是修改,一些读取操作本身也会导致重排进而导致重绘。和直觉不太一样吧?为什么仅仅是读取也会影响重排呢?那么就要了解读取本身做了什么。
读取一个元素的节点信息,那么我的期望是得到的是当前元素的状态。但对于浏览器而言它无法判断render树里存储的是否是最新的状态,那么只好是先回流得到最精确的状态返回出来。所以我虽然只是期望读取节点状态,但实际触发了一次回流。
那什么情况会导致重排?
- 修改元素大小、位置、数目
- 修改元素内容:图片大小、文字数量
- 滚动到..
- 读取刷新队列节点信息
- ...
具体可参考: https://gist.github.com/paulirish/5d52fb081b3570c81e3a
合成
对于浏览器而言,在回流与绘制之后,实际还有一件事要做,它叫做合成。
[浏览器一般渲染流程]
那什么是合成呢?
先通过直觉来说,我在纸上画画,按照我的设想在对应区域画出了元素并给他上了颜色,这时候这幅画就完成了。但如果这时候我对某个区域不满意了,我想要修改它,我不能说把纸上这块区域裁掉再糊张纸打补丁,我只能说是用颜料先给他涂成背景色,再在上面继续作画。这时候虽然看着我的画被更新了,但实际原本的内容还留存着,只是被颜料盖住了很难做图层分离而已。
但对于浏览器而言这是很好处理的,她会根据render树把元素的布局和效果绘制出来,如果这时候有重叠的元素,那么就会对重叠的部分做合成,合成完之后更新页面。
那么什么情况下会触发合成呢?
根据上面的解释,在有重叠的部分会触发合成,那么一般来说拥有像absolut、fixed等包含绝对定位的、trasnform包含z轴属性的、opacity透明度、filter滤镜等样式的元素会触发合成。
但是合成本身也是一种昂贵的开销(意味着新的内存分配和更复杂的层的管理),所以不要一味的提升合成层,一般只对复杂的动画做合成层的提升来说是比较好的。
讲了这么多,怎么优化呢? 根据上面的说法,我们已经知道了浏览器渲染的工作原理。 那么在搬砖的时候怎么做优化呢?
js:
-
尽可能少的读取节点属性,如果必需要那就尽可能多的提取为公共变量,统一使用。
比如遍历前先获取一次,那么遍历的时候只需要使用就可以,而不是每次遍历都获取一次 -
尽可能少的操作节点属性,如果必需要那就一次性做修改。
比如把要修改的属性都写到style属性里,而不是对属性做逐个修改。 -
尽可能少的增删节点。
如果要遍历增加可以先用createDocumentFragment创建节点,增加完后再一次性加入目标节点。
css
- 尽可能指定盒子大小
- 高开销节点尽可能脱离文档流,避免导致父节点被迫更新
- 使用硬件加速
transform、opacity、filters、Will-change这类css属性会开启硬件加速不会引起回流重绘
(后续再有再补充...)