重排与重绘

浏览器渲染过程

  1. 解析HTML,生成DOM树,解析CSS,生成CSSOM树
  2. 将DOM树和CSSOM树结合,生成渲染树(Render Tree)
  3. Layout(排布):根据生成的渲染树,进行排布(Layout),得到节点的几何信息(位置,大小)
  4. Painting(绘制):根据渲染树以及排布得到的几何信息,得到节点的绝对像素
  5. Display:将像素发送给GPU,展示在页面上。(这一步其实还有很多内容,比如会在GPU将多个合成层合并为同一个层,并展示在页面中。而css3硬件加速的原理则是新建合成层)

Browser Rendering Process

重排与重绘

任何改变用来构建渲染树的信息都会导致一次重排或重绘。

  • 重排
    部分渲染树(或者整个渲染树)需要重新分析并且节点尺寸需要重新计算
  • 重绘
    由于节点的几何属性发生改变或者由于样式发生改变,例如改变元素背景色时,屏幕上的部分内容需要更新

注意:

  • 重绘不一定导致重排,重排一定导致重绘
  • 重排开销比重绘大
实例 重排 重绘
bstyle.padding = "20px";
bstyle.border = "10px solid red";
bstyle.color = "blue";
bstyle.backgroundColor = "#red";
bstyle.fontSize = "2em";
document.body.appendChild(document.createTextNode('dude!'));
浏览器尺寸发生改变
元素位置或尺寸发生改变
新增或删除可见元素
激活CSS伪类
visibilityoutlinebackground-color

最小化重排重绘

浏览器的优化

浏览器会基于你的脚本要求创建一个变化的队列,然后分批去展现。通过这种方式许多需要一次重排的变化就会整合起来,最终只有一次重排会被计算渲染。浏览器可以向已有的队列中添加变更并在一个特定的时间或达到一个特定数量的变更后执行。

但是当程序员在代码中,需要直接获取样式信息时,为了提供最新的样式信息,浏览器被迫刷新队列,这时会引发一次重排重绘。

优化方法

  • 不要逐个修改样式,通过改变类名编辑cssText方式来修改

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // bad 
    var left = 10, top = 10;
    el.style.left = left + "px";
    el.style.top = top + "px";

    // change class name
    el.className += " theclassname";

    // edit cssText
    el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
  • 修改大量属性时,先通过display:none将元素隐藏(触发一次重排),批量修改完属性后再通过display显示出来(触发第二次重排),这样修改大量属性也只触发两次重排。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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';
  • 使用文档片段(Document Fragment),构建子树,在子树中修改,完成后再接入DOM中

    1
    2
    3
    4
    const ul = document.getElementById('list');
    const fragment = document.createDocumentFragment();
    appendDataToElement(fragment, data);
    ul.appendChild(fragment);
  • 复制要更新的节点,修改节点的副本,修改完成后,再使用副本替换树上的节点。

    1
    2
    3
    4
    const ul = document.getElementById('list');
    const clone = ul.cloneNode(true);
    appendDataToElement(clone, data);
    ul.parentNode.replaceChild(clone, ul);
  • 不要频繁取样式,样式取出来后存在变量里。

    优化前,每次循环的时候,都读取了box的一个offsetWidth属性值,然后利用它来更新p标签的width属性。这就导致了每一次循环的时候,浏览器都必须先使上一次循环中的样式更新操作生效,才能响应本次循环的样式读取操作。每一次循环都会强制浏览器刷新队列。

    1
    2
    3
    4
    5
    function initP() {
    for (let i = 0; i < paragraphs.length; i++) {
    paragraphs[i].style.width = box.offsetWidth + 'px';
    }
    }

    优化后

    1
    2
    3
    4
    5
    6
    const width = box.offsetWidth;
    function initP() {
    for (let i = 0; i < paragraphs.length; i++) {
    paragraphs[i].style.width = width + 'px';
    }
    }
  • 对于多次重排的元素,比如说动画。使用绝对定位脱离文档流,使其不影响其他元素。