CSS面试20真题高频版-回流重绘?什么场景触发?如何减少?

72 阅读5分钟

浏览器解析渲染机制

  • 解析HTML,生成DOM树
  • 解析CSS,生成CSSOM树
  • 将DOM树和CSSOM树结合,生成渲染树(RenderTree)
  • Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
  • Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
  • Display:将像素发送给GPU,展示在页面上

在页面初始渲染阶段,回流不可避免的触发,可以理解成页面一开始是空白的元素,后面添加了新的元素使页面布局发生改变 当我们对 DOM 的修改引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性,然后再将计算的结果绘制出来当我们对 DOM 的修改导致了样式的变化(color 或 background-color ),却并未影响其几何属性时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式,这里就仅仅触发了重绘

什么是回流重绘?

回流

回流(Layout)(也称为布局重排)是指浏览器根据网页内容计算并确定网页布局尺寸位置构建渲染树(Render Tree)过程。回流通常发生在以下情况:

  • 添加、删除或修改DOM元素
  • 改变DOM元素的尺寸、位置(如设置 widthheightpaddingbordermargin
  • 改变视口(用户调整窗口大小或滚动页面)
  • 页面初始渲染
  • 激活CSS伪类(如 :hover)。
  • 查询某些属性或调用某些方法(如offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth.scrollHeight、clientTop、clientLeft、clientWidth、clientHeight 、getComputedstyle等时会引发回流) 这些属性有一个共性,就是需要通过即时计算得到。因此浏览器为了获取这些值,也会进行回流

回流的影响范围可分为全局和局部。全局回流会影响整个页面的布局,导致重新计算所有元素的样式和位置;而局部回流只会影响某些特定元素及其后代。回流的成本通常比重绘大得多,因此在前端开发中,应尽量减少回流的发生

重绘

重绘(Painting) 是指浏览器根据新的样式信息重新绘制DOM元素的过程,但不会改变布局。重绘通常发生在以下情况:

  • 改变元素的颜色、背景、边框等样式属性
  • 元素的可见性发生变化(如display: none变为display: block先发生回流后触发重绘)
  • 字体样式等

值得注意的是,回流一定会引起重绘,但重绘不一定会导致回流。例如,改变一个元素的颜色时,浏览器只会进行重绘操作;而改变一个元素的宽度时,浏览器会进行回流操作

浏览器优化机制

由于每次重排(回流)都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改批量执行来优化重排过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个值,才清空队列

如何减少回流

减少回流次数

  • 样式集中处理:将样式的修改集中到一个步骤中,而不是逐个修改。这可以通过创建一个样式数组,通过更改一个元素的 class 来实现样式的切换,从而减少操作次数。
// 1. 不集中处理样式(直接修改样式属性)
document.getElementById("update").addEventListener("click", () => {
    const boxes = document.querySelectorAll(".box");
    boxes.forEach((box) => {
        box.style.width = "200px"; // 直接修改样式属性
        box.style.height = "200px";
        box.style.backgroundColor = "red";
        box.style.transform = "translateX(50px)";
    });
});

// 1. 减少回流次数:通过集中处理样式
document.getElementById("update").addEventListener("click", () => {
    const boxes = document.querySelectorAll(".box");
    boxes.forEach((box) => {
        box.classList.add("new-style"); // 通过添加类名来集中更新样式
    });
});
  • 避免设置多项内联样式
  • 通过变量存储调用 offsetWidth 结果:当需要多次获取元素的布局信息(如 offsetWidth),应将其存储在变量中以避免重复计算,从而减少回流。
// 2. 不使用变量存储 `offsetWidth` 结果(多次调用 `offsetWidth`)
document.getElementById("addBox").addEventListener("click", () => {
    const container = document.getElementById("container");
    console.log("Container Width:", container.offsetWidth); // 多次调用 offsetWidth
    console.log("Container Width:", container.offsetWidth); // 多次调用 offsetWidth
    console.log("Container Width:", container.offsetWidth); // 多次调用 offsetWidth
    const newBox = document.createElement("div");
    newBox.className = "box new-style";
    container.appendChild(newBox);
});

// 2. 使用变量存储调用 offsetWidth 结果:避免重复计算  
const container = document.getElementById('container');  
const containerWidth = container.offsetWidth;  // 只计算一次  
console.log('Container Width:', containerWidth); // 输出容器宽度

减少回流范围

核心观念是通过精简涉及回流的DOM操作来减小影响范围。常见减少回流范围的操作包括:

  • DOM离线操作:在进行多次DOM更新时,可以使用文档片段(DocumentFragment)来批量更新DOM,避免频繁的回流。
// 3. DOM 离线操作:使用文档片段添加多个元素  
document.getElementById('addBox').addEventListener('click', () => {  
    const fragment = document.createDocumentFragment();  
    const newBox = document.createElement('div');  
    newBox.className = 'box new-style'; // 直接添加样式类  
    fragment.appendChild(newBox);  
    // 一次性添加,避免多次回流  
    container.appendChild(fragment);  
});

// 3. 不使用文档片段(直接添加元素到 DOM)
document.getElementById("addBox").addEventListener("click", () => {
    const container = document.getElementById("container");
    const newBox = document.createElement("div");
    newBox.className = "box new-style";
    container.appendChild(newBox); // 直接添加元素到 DOM
    const newBox2 = document.createElement("div");
    newBox2.className = "box new-style";
    container.appendChild(newBox2); // 直接添加元素到 DOM
    const newBox3 = document.createElement("div");
    newBox3.className = "box new-style";
    container.appendChild(newBox3); // 直接添加元素到 DOM
});
  • 脱离文档流:对于一些大型操作,使用绝对定位或固定定位可以将元素脱离文档流,从而减少回流的影响。
// 4. 不脱离文档流(使用默认的文档流布局)
const floatingBox = document.createElement("div");
floatingBox.className = "box";
floatingBox.style.top = "150px"; // 位置
floatingBox.style.left = "150px"; // 位置
document.body.appendChild(floatingBox); // 直接添加到文档中,不脱离文档流 

// 4. 脱离文档流:使用绝对定位可减少回流影响  
const floatingBox = document.createElement('div');  
floatingBox.className = 'box';  
floatingBox.style.position = 'absolute'; // 脱离文档流  
floatingBox.style.top = '150px'; // 位置  
floatingBox.style.left = '150px'; // 位置  
document.body.appendChild(floatingBox); // 直接添加到文档中