你真的了解回流和重绘吗?请看这里

1,401 阅读8分钟

要了解回流与重绘,首先得知道浏览器的渲染过程

浏览器的渲染过程

  1. 关键渲染路径: 指的是浏览器从最初接收到的HTML,CSS,JavaScript等资源,然后解析,构建树,渲染布局,绘制,最终呈现给客户能看到的界面的过程.
  2. 实际用户所看到的只有二个阶段:DOMContentLoaded(页面内容加载完成)和Load(页面资源加载完成).
    • 页面内容加载完成:DOMContentLoaded事件触发时,只是DOM加载完成,不包括样式表,图片等
    • 页面资源加载完成:load事件触发时,页面上所有的DOM,样式表,脚本图片都已经加载完成
  3. 浏览器的渲染图如下:
  4. 渲染图解释:
    • 浏览器将请求到的HTML文档解析成DOM树.
    • 将CSS样式表=>CCSSOM(CSS Object Model)即层叠样式表模型(浏览器解析CSS文件并生成CSSOM,每个CSS文件都被分析成一个StyleSheet对象,每个对象都包含CSS规则。CSS规则对象包含对应于CSS语法的选择器和声明对象以及其他对象)
    • Attchment:将 DOM树 和 CCSSOM 合并(Attchment)生成渲染树(Render Tree),生成的渲染对象等待渲染(通过DOM树和CSS规则树,浏览器就可以通过它两构建渲染树了。浏览器会先从DOM树的根节点开始遍历每个可见节点,然后对每个可见节点找到适配的CSS样式规则并应用)
    • Layout:渲染树的每一个元素的内容都是计算过的,得到节点的几何信息(位置,大小)等,我们称为布局(Layout).浏览器使用流式布局的方式,只需要一次绘制就能够布局所以元素(布局阶段会从渲染树的根节点开始遍历,由于渲染树的每个节点都是一个Render Object对象,包含宽高,位置,背景色等样式信息。所以浏览器就可以通过这些样式信息来确定每个节点对象在页面上的确切大小和位置,布局阶段的输出就是我们常说的盒子模型,它会精确地捕获每个元素在屏幕内的确切位置与大小)
    • painting:根据渲染树以及回流得到的几何信息,得到节点的绝对像素,将渲染树的各个节点绘制(painting)到屏幕上
    • Display:将像素发送给GPU,展示在页面上。(这一步其实还有很多内容,比如会在GPU将多个合成层合并为同一个层,并展示在页面中。而css3硬件加速的原理则是新建合成层)
    • 注意:以上步骤并不一定一次行的顺序完成,如果DOM或者CSSOM被修改了,或者有的步骤里某些过程可能重复操作,在平时开发的时候,JavaScript的某些操作和CSS赋值往往会多次修改DOM或者CSSOM。

流程详解

  1. 构建DOM树

    • DOM树在构建的过程中可能会被CSS和js的加载而阻塞执行
    • 对于display:none的元素也会在DOM树的
    • 注释还有SCRIPT标签都在DOM树
    • 只有当前节点的所有子节点全部构建好才会去构建当前节点的下一个兄弟节点
  2. 构建CSSOM树

    • CSS解析、DOM解析可以同时进行
    • CSS的解析与script的执行是互斥的
    • 在Webkit内核中进行了script执行优化,只有在JS访问CSS时才会发生互斥
  3. 构建渲染树

    • 渲染树与DOM树不完全对应(渲染树只包含可见节点)
    • display:none的元素不在渲染树中
    • visibility: hidden的元素在渲染树中
    • 渲染树生成后,需要通过布局(Layout)的处理,渲染到屏幕需要得到各个节点的位置信息
  4. 渲染树布局(layout of the render tree)

    • 脱离文档流,其实就是脱离渲染树
    • float元素,absoulte元素,fixed元素会发生位置偏移,也叫脱离文档流
  5. 渲染树绘制(Painting the render tree)

    • 在绘制阶段,浏览器会遍历渲染树,调用渲染器的paint()方法在屏幕上显示其内容,渲染树的绘制工作是由浏览器的UI后端组件完成的

基础的知识点大概就这些了,有了这些知识点的支撑,我们来讲讲回流(reflow)与重绘(repaint)

回流和重绘(reflow)

DOM默认的布局方式是流式布局,但是js和css会打破这种布局,改变DOM的外观以及大小、位置,所有就有了reflowrepaint 而且回流一定会触发重绘,而重绘不一定会回流

回流(reflow)

  1. 通过构造渲染树,将可见DOM节点以及它对应的样式结合起来,可是还需要计算它们在设备视口(viewport)内的确切位置和大小,这个计算的阶段就是回流,为了弄清每个对象在网站上的确切大小和位置,浏览器从渲染树的根节点开始遍历
  2. 当然当浏览器发现布局发生了变化,这个时候就需要倒回去重新渲染,这个回退的过程叫reflow
  3. 其实页面一开始渲染的时候之所以叫回流,我的理解是,页面渲染之前那个空白页其实也是有内容的,从"空白"到有元素显示这就是回流与重绘

重绘(repaint)

  1. 通过构造渲染树和回流阶段,我们知道了哪些节点是可见的,以及可见节点的样式和具体的几何信息(位置、大小),那么我们就可以将渲染树的每个节点都转换为屏幕上的实际像素,这个阶段就叫做重绘

何时发生回流(reflow)与重绘(repaint)

  1. 页面第一次渲染(初始化)
  2. DOM树变化(如:增删节点)
  3. Render树变化(如:padding改变)
  4. 浏览器窗口resize
  5. 获取元素的某些属性和方法(例如)
    • offsetTop、offsetLeft、offsetWidth、offsetHeight
    • scrollTop、scrollLeft、scrollWidth、scrollHeight
    • clientTop、clientLeft、clientWidth、clientHeight
    • getComputedStyle()
    • getBoundingClientRect()返回元素的大小及其相对于视口的位置
    • 背景色、颜色、字体改变(注意:字体大小发生变化时,会触发回流)
    • 具体可以访问这个网站:gist.github.com/paulirish/5… (有可能打不开,可以多次刷新试试)
  6. 回流必定引起t重绘重绘可以单独触发

浏览器的优化机制

现代的浏览器都是很聪明的,由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列。但是!当你获取布局信息的操作的时候,会强制队列刷新,比如上面的获取属性与操作方法都需要返回最新的布局信息,因此浏览器不得不清空队列,触发回流重绘来返回正确的值。因此建议,我们在修改样式的时候,**最好避免使用上面列出的属性,他们都会刷新渲染队列。**如果要使用它们,最好将值缓存起来。

减少回流和重绘

  1. css3硬件加速(GPU加速)

    • 可以让transform、opacity、filters这些动画不会引起回流重绘
    • Will-change属性(没用过)
  2. 避免逐个修改节点样式,尽量一次性修改(使用cssText或者修改CSS的class)

    const el = document.getElementById('test');
    el.style.padding = '5px';
    el.style.borderLeft = '1px';
    el.style.borderRight = '2px';
    //避免上面的,下面的合适
    const el = document.getElementById('test');
    el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;'
    
    const el = document.getElementById('test');
    el.className += ' active';
    
  3. 使用DocumentFragment将需要多次修改的DOM元素缓存,最后一次性append到真实DOM中渲染

    function appendDataToElement(appendToElement, data) {
        let li;
        for (let i = 0; i < data.length; i++) {
        	li = document.createElement('li');
            li.textContent = 'text';
            appendToElement.appendChild(li);
        }
    }
    const ul = document.getElementById('list');
    const fragment = document.createDocumentFragment();
    appendDataToElement(fragment, data);
    ul.appendChild(fragment);
    
  4. 可以将需要多次修改的DOM元素设置display:none,操作完再显示。(因为隐藏元素不在render树内,因此修改隐藏元素不会触发回流重绘)

    function appendDataToElement(appendToElement, data) {
        let li;
        for (let i = 0; i < data.length; i++) {
        	li = document.createElement('li');
            li.textContent = 'text';
            appendToElement.appendChild(li);
        }
    }
    const ul = document.getElementById('list');
    ul.style.display = 'none';
    appendDataToElement(ul, data);
    ul.style.display = 'block';
    
  5. 避免多次读取某些属性(避免触发同步布局事件)

    function initP() {
        for (let i = 0; i < paragraphs.length; i++) {
            paragraphs[i].style.width = box.offsetWidth + 'px';
        }
    }
    //这二种写法后面的性能比前面得要好很多
    const width = box.offsetWidth;
    function initP() {
        for (let i = 0; i < paragraphs.length; i++) {
            paragraphs[i].style.width = width + 'px';
        }
    }
    
  6. 通过绝对位移将复杂的节点元素脱离文档流,形成新的Render Layer,降低回流成本

    function appendDataToElement(appendToElement, data) {
        let li;
        for (let i = 0; i < data.length; i++) {
        	li = document.createElement('li');
            li.textContent = 'text';
            appendToElement.appendChild(li);
        }
    }
    const ul = document.getElementById('list');
    const clone = ul.cloneNode(true);
    appendDataToElement(clone, data);
    ul.parentNode.replaceChild(clone, ul);
    
  7. 对于复杂动画效果,使用绝对定位让其脱离文档流

总结

前端要学的东西太多了,一步一个脚印,加油,对了,记得点赞,谢谢