CSS 重排 重绘 合成

前言

  • 重排、重绘、合成是什么?总的来说,它们都是浏览器渲染页面进程中的一个小小的环节,正是这些小环节按照一定的规则有条不紊的运作,才保证了能正常、顺畅地网上冲浪。
  • 主要介绍了重排、重绘、合成的基本概念、触发时机、影响范围以及其优化策略。
  • 首先来看一下,浏览器的渲染原理,看一下,它们分别担任了什么工作?

    一、浏览器渲染原理

    CSS 重排、重绘和合成 - 图1结合上图,一个完整的渲染流程大致可总结为如下几个步骤:
  1. HTML被HTML解析器解析成DOM Tree
  2. CSS则被CSS解析器解析成CSSOM Tree
  3. DOM Tree和CSSOM Tree解析完成后,被附加到一起,形成渲染树(Render Tree)
  4. 布局,根据渲染树计算每个节点的几何信息生成布局树(Layout Tree)
  5. 对布局树进行分层,并生成分层树(Layer Tree)
  6. 为每个图层生成绘制列表
  7. 渲染绘制(Paint)。根据计算好的绘制列表信息绘制整个页面,并将其提交到合成线程
  8. 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图,发送绘制图块命令 DrawQuad 给浏览器进程
  9. 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上

    二、重排

  10. 定义:当通过JS或css改变了元素的宽度、高度等,修改了元素的几何位置属性,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。无疑,重排需要更新完整的渲染流水线,所以开销也是最大的。

  11. 触发时机和影响范围:DOM节点信息更改,触发重排时,这个DOM更改程度会决定周边DOM更改范围。

    • 全局范围:就是从根节点html开始对整个渲染树进行重新布局,例如当改变了窗口尺寸或方向或者是修改了根元素的尺寸或者字体大小等。
    • 局部范围:对渲染树的某部分或某一个渲染对象进行重新布局。

      三、重绘

  12. 定义:如果修改了元素的背景颜色,并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。

  13. 触发时机和影响范围:每一次的dom更改或者css几何属性更改,都会引起一次浏览器的重排/重绘过程,而如果是css的非几何属性更改,则只会引起重绘过程。所以说重排一定会引起重绘,而重绘不一定会引起重排,重绘的开销较小,重排的代价较高。

    四、合成

  14. 定义:合成是一种将页面的各个部分分离成层(Layer Tree),分别将它们栅格化,然后在称为“合成线程”的中组合为页面的技术。

  15. 触发时机和影响范围:在GUI渲染线程后执行,将GUI渲染线程生成的绘制列表转换为位图,然后发送绘制图块命令 DrawQuad 给浏览器进程,浏览器进程根据 DrawQuad 消息生成页面,将页面显示到显示器上
  16. 优点:使用了 CSS 的 transform 来实现动画效果,避开了重排和重绘阶段,直接在非主线程上执行合成动画操作。这样的效率是最高的,因为是在非主线程上合成,并没有占用主线程的资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率。

    五、常见的触发重排、重绘的属性和方法

    1. 常见的触发重排、重绘的属性和方法

    CSS 重排、重绘和合成 - 图2

    2. 全局范围重排、局部范围重排、重绘的影响范围示例

    CSS 重排、重绘和合成 - 图3

    六、优化策略

    6.1 减少DOM操作

  17. 最小化DOM访问次数,尽量缓存访问DOM的样式信息,避免过度触发重排。

  18. 如果在一个局部方法中需要多次访问同一个dom,可以在第一次获取元素时用变量保存下来,减少遍历时间。
  19. 用事件委托来减少事件处理器的数量。
  20. querySelectorAll()替代getElementByXX()
  • querySelectorAll():获取静态集合,通过函数获取元素之后,元素之后的改变并不会影响之前获取后存储到的变量。也就是获取到元素之后就和html中的这个元素没有关系了
  • getElementByXX():获取动态集合,通过函数获取元素之后,元素之后的改变还是会动态添加到已经获取的这个元素中。换句话说,通过这个方法获取到元素存储到变量的时候,以后每一次在Javascript函数中使用这个变量的时候都会再去访问一下这个变量对应的html元素。

    6.2 减少重排

  1. 放弃传统操作DOM的时代,基于vue/react开始数据影响视图模式。
  2. 避免设置大量的style内联属性,因为通过设置style属性改变结点样式的话,每一次设置都会触发一次reflow,所以最好是使用class属性。
  3. 不要使用table布局,因为table中某个元素一旦触发了reflow,那么整个table的元素都会触发reflow。那么在不得已使用table的场合,可以设置table-layout:auto;或者是table-layout:fixed这样可以让table一行一行的渲染,这种做法也是为了限制reflow的影响范围。
  4. 尽量少使用display:none可以使用visibility:hidden代替,display:none会造成重排,visibility:hidden只会造成重绘。
  5. 使用resize事件时,做防抖和节流处理。
  6. 分离读写操作(现代的浏览器都有渲染队列的机制)
  • 分离读写减少重排的原理
    1. <style>
    2. #box{
    3. width:100px;
    4. height:100px;
    5. border:10px solid #ddd;
    6. }
    7. </style>
    8. <body>
    9. <div id="box"></div>
    10. <script>
    11. //读写分离,一次重排
    12. let box = document.getElementById('box')
    13. box.style.width='200px';//(写)js改变样式,加入渲染队列中,顿一下,查看下一行是否还是修改样式,如果是则再加入到渲染队列,一直到下一行代码不是修改样式为止
    14. box.style.height='200px';//(写)
    15. box.style.margin='10px';//(写)
    16. console.log(box.clientWidth);//(读)
    17. </script>
    18. </body>
    1. <script>
    2. //没做到读写分离,两次重排
    3. box.style.width='200px';//(写)js改变样式,加入渲染队列中,顿一下,下一行不是修改样式的代码,浏览器就会直接渲染一次(重排)
    4. console.log(box.clientWidth);//(读)
    5. box.style.height='200px';//(写)
    6. box.style.margin='10px';//(写)
    7. </script>
  1. 缓存布局信息

    1. <script>
    2. //两次重排 ’写‘操作中包含clientWidth属性,会刷新渲染队列
    3. box.style.width = box.clientWidth +10 +’px’;
    4. box.style.height= box.clientHeight +10 +’px
    5. </script>
    1. <script>
    2. let a=box.clientWidth //(读)缓存布局信息
    3. let b=box.clientHeight//(读)缓存布局信息
    4. //一次重排
    5. box.style.width = a+10 +’px’;(写)
    6. box.style.height= b+10 +’px’(写)
    7. </script>

    注意:offsetTopoffsetLeftoffsetWidthoffsetHeightclientTopclientWidthclientHeightscrollTopscrollLeftscrollWidthscrollHeightgetComputedStylecurrentStyle…会刷新渲染队列。当下一行代码有这些时,即使下一行是修改样式,也会直接发生重排。

    6.3 css及优化动画

  2. 少用css表达式

  3. 样式集中改变(批量处理) 减少通过JavaScript代码修改元素样式,尽量使用修改class名方式操作样式或动画;
  4. 可以把动画效果应用到position属性为absolutefixed的元素上,这样对其他元素影响较小
  5. 动画实现的速度的选择:牺牲平滑度换取速度。比如实现一个动画,以1个像素为单位移动这样最平滑,但是reflow就会过于频繁,大量消耗CPU资源,如果以3个像素为单位移动则会好很多。
  6. 开启css3动画硬件加速(GPU加速)把渲染计算交给GPU。(能用transform做的就不要用其他的,因为transform可以开启硬件加速,而硬件加速可以规避重排。直接跳过重排、重绘,走合成进程)
    1. box.style.left='100px' //向右移动100px,一次重排
    2. box.style.ctransform='translateX(200)' //向右移动200px,不会引发重排