首先仍然不得不提的是 “在客户端拿到 HTML 后的处理”:
-
从上到下解析 HTML 文档生成DOM树;
-
加载解析样式构建CSSOM树;
-
加载并执行JavaScript代码;
-
根据DOM树和CSSOM树,生成 render 树;
-
渲染;
-
布局;
-
绘制
我们可能很多次听到过:“要尽可能地减少重排和重绘,因为它们会影响浏览器性能。”
但,为什么呢?
事实上,一个页面是由许多层级组成的(就像千层饼一样) —— 这里的“层级”指的是“ DOM 元素渲染层(Layer)”。一个页面在构建完 render tree 到展现在我们面前还经历了一个“特别的流程”:
-
浏览器会先获取DOM树并依据样式将其分割成多个独立的渲染层
-
CPU 将每一层绘制进位图中
-
将位图作为纹理上传至 GPU(显卡)绘制
-
GPU 将所有的渲染层缓存并复合多个渲染层最终形成我们的图像(如果下次上传的渲染层没有发生变化,GPU 就不需要对其进行重新绘制)
(:从上面的步骤我们可以知道:布局是CPU处理的,而绘制是由GPU完成的。
就像这张图说的(from Firefox的3D View插件的页面Layers层级图)
问题就发生在上面所说流程的第2、4步中。大家试想一下:如果我们把那些会发生复杂运动/变化或一直发生大量重排重绘的元素提起出来,单独放在一个渲染层触发,那它就不会连累其他元素了!
那什么情况下会触发渲染层呢?
比如 video
、WebGL
、Canvas
、CSS3 3D
、CSS滤镜
、z-index大于某个相邻节点的值
的元素都会触发新的Layer —— 这里要理解一点:它并不单单指 z-index!这里极力推荐张鑫旭大大的这一篇文章:深入理解CSS中的层叠上下文和层叠顺序
比较简单的方法是,给元素加上下面的样式:
transform: translateZ(0); backface-visibility: hidden;
我们把容易触发重排重绘的元素单独触发渲染层,让它与那些“静态”元素隔离,让 GPU 分担更多的渲染工作,我们通常把这样的措施成为硬件加速,或者是 GPU 加速。大家之前肯定听过这个说法 —— 就比如CSS中的 will-change
。
不论是重排还是重绘,都会阻塞浏览器。要提高网页性能,就要降低重排和重绘的频率和成本,近可能少地触发重新渲染。正如我们上面提到的:重排是由 CPU 处理的,而重绘是由 GPU 处理的,CPU 的处理效率远不及 GPU,并且重排一定会引发重绘,而重绘不一定会引发重排。所以在性能优化工作中,我们更应当着重减少重排的发生。
还有什么可以优化的?
-
CSS 属性读写分离:浏览器没次对元素样式进行读操作时,都必须进行一次重新渲染(重排 + 重绘),所以我们在使用 JS 对元素样式进行读写操作时,最好将两者分离开,先读后写,避免出现两者交叉使用的情况
-
通过切换 class 或者
style.csstext
属性去批量操作元素样式
-
DOM 元素离线更新:当对 DOM 进行相关操作时,例、
appendChild
等都可以使用 documentFragment
对象进行离屏操作,带元素“组装”完成后再一次插入页面,或者使用 display:none
对元素隐藏,在元素“消失”后进行相关操作,然后再显示出来
-
visibility: hidden
是个好东西,它既有display的隐藏,又有opacity的占位。而且它还支持移动动画
-
图片在渲染前指定大小:因为 img 元素是内联元素,所以在加载图片后会改变宽高,严重的情况会导致整个页面重排,所以最好在渲染前就指定其大小,或者让其脱离文档流