本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com
前言
重排、重绘、合成是什么?总的来说,它们都是浏览器渲染页面进程中的一个小小的环节,正是这些小环节按照一定的规则有条不紊的运作,才保证了我们能正常、顺畅地网上冲浪。
本文主要介绍了重排、重绘、合成的基本概念、触发时机、影响范围以及其优化策略。
我们首先来看一下,浏览器的渲染原理,看一下,它们分别担任了什么工作?
一、浏览器渲染原理
结合上图,一个完整的渲染流程大致可总结为如下几个步骤:
HTML 被 HTML 解析器解析成 DOM Tree
CSS 则被 CSS 解析器解析成 CSSOM Tree
DOM Tree 和 CSSOM Tree 解析完成后,被附加到一起,形成渲染树(Render Tree)
布局,根据渲染树计算每个节点的几何信息生成布局树(Layout Tree)
对布局树进行分层,并生成分层树(Layer Tree)
为每个图层生成绘制列表
渲染绘制 (Paint)。根据计算好的绘制列表信息绘制整个页面,并将其提交到合成线程
合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图,发送绘制图块命令 DrawQuad 给浏览器进程
浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上
二、重排
定义:当通过 JS 或 css 改变了元素的宽度、高度等,修改了元素的几何位置属性,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。无疑,重排需要更新完整的渲染流水线,所以开销也是最大的。
触发时机和影响范围:DOM 节点信息更改, 触发重排时,这个 DOM 更改程度会决定周边 DOM 更改范围。
全局范围:就是从根节点 html 开始对整个渲染树进行重新布局,例如当我们改变了窗口尺寸或方向或者是修改了根元素的尺寸或者字体大小等。
局部范围:对渲染树的某部分或某一个渲染对象进行重新布局。
三、重绘
定义:如果修改了元素的背景颜色,并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。
触发时机和影响范围:每一次的 dom 更改或者 css 几何属性更改,都会引起一次浏览器的重排 / 重绘过程,而如果是 css 的非几何属性更改,则只会引起重绘过程。所以说重排一定会引起重绘,而重绘不一定会引起重排,重绘的开销较小,重排的代价较高。
四、合成
定义:合成是一种将页面的各个部分分离成层(Layer Tree),分别将它们栅格化,然后在称为 “合成线程” 的中组合为页面的技术。
触发时机和影响范围:在 GUI 渲染线程后执行,将 GUI 渲染线程生成的绘制列表转换为位图, 然后发送绘制图块命令 DrawQuad 给浏览器进程,浏览器进程根据 DrawQuad 消息生成页面,将页面显示到显示器上
优点:我们使用了 CSS 的 transform 来实现动画效果,避开了重排和重绘阶段,直接在非主线程上执行合成动画操作。这样的效率是最高的,因为是在非主线程上合成,并没有占用主线程的资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率。
五、常见的触发重排、重绘的属性和方法
- 常见的触发重排、重绘的属性和方法
- 全局范围重排、局部范围重排、重绘的影响范围示例
六、优化策略
6.1 减少 DOM 操作
最小化 DOM 访问次数,尽量缓存访问 DOM 的样式信息,避免过度触发重排。
如果在一个局部方法中需要多次访问同一个 dom,可以在第一次获取元素时用变量保存下来,减少遍历时间。
用事件委托来减少事件处理器的数量。
用 querySelectorAll() 替代 getElementByXX()。
querySelectorAll():获取静态集合,通过函数获取元素之后,元素之后的改变并不会影响之前获取后存储到的变量。也就是获取到元素之后就和 html 中的这个元素没有关系了
getElementByXX():获取动态集合,通过函数获取元素之后,元素之后的改变还是会动态添加到已经获取的这个元素中。换句话说,通过这个方法获取到元素存储到变量的时候,以后每一次在 Javascript 函数中使用这个变量的时候都会再去访问一下这个变量对应的 html 元素。
6.2 减少重排
放弃传统操作 DOM 的时代,基于 vue/react 开始数据影响视图模式。
避免设置大量的 style 内联属性,因为通过设置 style 属性改变结点样式的话,每一次设置都会触发一次 reflow,所以最好是使用 class 属性。
不要使用 table 布局,因为 table 中某个元素一旦触发了 reflow,那么整个 table 的元素都会触发 reflow。那么在不得已使用 table 的场合,可以设置 table-layout:auto; 或者是 table-layout:fixed 这样可以让 table 一行一行的渲染,这种做法也是为了限制 reflow 的影响范围。
尽量少使用 display:none 可以使用 visibility:hidden 代替,display:none 会造成重排,visibility:hidden 只会造成重绘。
使用 resize 事件时,做防抖和节流处理。
分离读写操作(现代的浏览器都有渲染队列的机制)
- 分离读写减少重排的原理
<style> #box{ width:100px; height:100px; border:10px solid #ddd; }</style><body> <div id="box"></div> <script> //读写分离,一次重排 let box = document.getElementById('box') box.style.width='200px';//(写)js改变样式,加入渲染队列中,顿一下,查看下一行是否还是修改样式,如果是则再加入到渲染队列,一直到下一行代码不是修改样式为止 box.style.height='200px';//(写) box.style.margin='10px';//(写) console.log(box.clientWidth);//(读) </script></body><script> //没做到读写分离,两次重排 box.style.width='200px';//(写)js改变样式,加入渲染队列中,顿一下,下一行不是修改样式的代码,浏览器就会直接渲染一次(重排) console.log(box.clientWidth);//(读) box.style.height='200px';//(写) box.style.margin='10px';//(写) </script>缓存布局信息
<script> //两次重排 ’写‘操作中包含clientWidth属性,会刷新渲染队列 box.style.width = box.clientWidth +10 +’px’; box.style.height= box.clientHeight +10 +’px’</script><script> let a=box.clientWidth //(读)缓存布局信息 let b=box.clientHeight//(读)缓存布局信息 //一次重排 box.style.width = a+10 +’px’;(写) box.style.height= b+10 +’px’(写)</script>注意:
offsetTop、offsetLeft、offsetWidth、offsetHeight、clientTop、clientWidth、clientHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、getComputedStyle、currentStyle...会刷新渲染队列。当下一行代码有这些时,即使下一行是修改样式,也会直接发生重排。
6.3 css 及优化动画
少用 css 表达式
样式集中改变(批量处理)减少通过 JavaScript 代码修改元素样式,尽量使用修改 class 名方式操作样式或动画;可以把动画效果应用到 position 属性为 absolute 或 fixed 的元素上,这样对其他元素影响较小
动画实现的速度的选择:牺牲平滑度换取速度。比如实现一个动画,以 1 个像素为单位移动这样最平滑,但是 reflow 就会过于频繁,大量消耗 CPU 资源,如果以 3 个像素为单位移动则会好很多。开启css3动画硬件加速(GPU加速)把渲染计算交给 GPU。(能用 transform 做的就不要用其他的,因为 transform 可以开启硬件加速,而硬件加速可以规避重排。直接跳过重排、重绘,走合成进程)
box.style.left='100px' //向右移动100px,一次重排 box.style.ctransform='translateX(200)' //向右移动200px,不会引发重排七、结束:
读到这里,重排、重绘、合成的概念,以及如何针对它们做前端的性能优化,大家有没有更清楚一点呢?如果文中有哪些地方写的不好、不对的地方,欢迎大家批评指正,感谢您的阅读,今天也是元气满满的一天,一起加油呦!