skip to content
月与羽

重排和重绘

/ 9 min read


浏览器渲染的基本流程

要理解重排和重绘,首先需要了解浏览器渲染页面的基本步骤:

  1. 解析 (Parse):浏览器解析 HTML 生成 DOM 树 (DOM Tree),解析 CSS 生成 CSSOM 树 (CSSOM Tree)。
  2. 渲染树 (Render Tree):将 DOM 树和 CSSOM 树结合起来,生成渲染树 (Render Tree)。渲染树只包含需要显示的节点和它们的样式信息(比如 display: none 的节点不会在渲染树中)。
  3. 布局 (Layout/Reflow):浏览器根据渲染树计算出每个节点在屏幕上的确切位置和大小。这个过程就是 布局,也常被称为 重排 (Reflow)
  4. 绘制 (Paint/Repaint):浏览器根据计算好的布局信息,将每个节点绘制到屏幕上(比如颜色、边框、阴影等)。这个过程就是 绘制,也常被称为 重绘 (Repaint)
  5. 合成 (Composite):对于有独立图层的元素(比如通过 transform 提升的),浏览器会将这些图层合成,最终显示在屏幕上。

一、重排 (Reflow / Layout)

1. 什么是重排?

当 DOM 元素的 几何属性 (如宽度、高度、位置、边距等) 发生变化,导致浏览器需要重新计算元素在设备视口内的几何大小和位置时,就会发生重排。

2. 为什么重排的开销很大?

一个元素的重排可能会导致其所有子元素、祖先元素以及页面上其他相关元素的几何属性重新计算。这就像多米诺骨牌效应,一个小的变动可能引发大规模的重新布局,非常消耗 CPU 资源。

3. 触发重排的常见操作:

  • 页面首次渲染:这是不可避免的第一次重排。
  • 添加或删除可见的 DOM 元素
  • 元素尺寸改变:改变 width, height, padding, margin, border-width 等。
  • 元素位置改变:改变 position, top, left, right, bottom 等。
  • 内容改变:例如,文本数量的增减、图片大小的改变导致元素尺寸变化。
  • 浏览器窗口尺寸改变 (resize 事件)。
  • 获取特定的布局信息:这是一个非常关键且容易被忽略的点。当你读取以下属性时,浏览器为了给你一个精确的值,会强制进行一次同步的布局(即立即重排),这个现象被称为 强制同步布局 (Forced Synchronous Layout)布局抖动 (Layout Thrashing)
    • offsetTop, offsetLeft, offsetWidth, offsetHeight
    • scrollTop, scrollLeft, scrollWidth, scrollHeight
    • clientTop, clientLeft, clientWidth, clientHeight
    • getComputedStyle()

二、重绘 (Repaint / Paint)

1. 什么是重绘?

当元素的 外观属性 (如颜色、背景、可见性等) 发生改变,但其几何属性没有变化时,浏览器会跳过布局阶段,直接进入绘制阶段,这个过程就是重绘。

2. 重绘的开销

重绘的开销远小于重排,因为它不涉及元素位置和大小的计算。但是,频繁的重绘仍然会占用 CPU 资源,影响性能。

3. 触发重绘的常见操作:

  • 改变 color, background-color, background-image
  • 改变 outline, visibility, text-decoration
  • 改变 box-shadow, border-radius, border-style (如果 border-width 不变)。

关系:重排必然触发重绘

重排必定会导致重绘,但重绘不一定会导致重排。

当你改变了元素的几何尺寸,浏览器需要重新布局(重排),布局完成后,为了将新的样式应用到屏幕上,必然会进行一次重绘。 而当你只改变元素的颜色时,布局没有变化,浏览器只需要重新绘制即可。


三、性能优化策略

优化的核心思想是:尽量减少重排和重绘的次数和范围。

1. 批量修改 DOM

避免在循环中逐一修改 DOM 元素的样式。应该将多次修改合并为一次。

不好的做法 (导致多次重排/重绘):

const el = document.getElementById('my-element');
for (let i = 0; i < 10; i++) {
el.style.top = (el.offsetTop + 10) + 'px'; // 读(offsetTop) -> 写 -> 读 -> 写 ... 造成布局抖动
el.style.left = (el.offsetLeft + 10) + 'px';
}

好的做法:

  • 使用 CSS class 统一修改

    // JS
    el.classList.add('animate');
    // CSS
    .animate {
    top: 100px;
    left: 100px;
    /* ...其他样式... */
    }
  • 离线操作 DOM:使用 DocumentFragment 或将元素 display: none 后再修改。

    const fragment = document.createDocumentFragment();
    for (let i = 0; i < 10; i++) {
    const newItem = document.createElement('li');
    newItem.textContent = `Item ${i}`;
    fragment.appendChild(newItem); // 在内存中操作,不触发重排
    }
    document.getElementById('my-list').appendChild(fragment); // 最后一次性插入DOM,只触发一次重排

2. 避免布局抖动 (Layout Thrashing)

将“读”操作(获取布局信息)和“写”操作(修改样式)分离开。

不好的做法:

const boxes = document.querySelectorAll('.box');
boxes.forEach(box => {
// 每次循环都立即读取宽度并设置,导致“读-写-读-写”循环
box.style.width = box.offsetWidth + 10 + 'px';
});

好的做法 (读写分离):

const boxes = document.querySelectorAll('.box');
const widths = [];
// 先全部读取
boxes.forEach(box => {
widths.push(box.offsetWidth);
});
// 再全部写入
boxes.forEach((box, i) => {
box.style.width = widths[i] + 10 + 'px';
});

3. 使用 transformopacity 实现动画

现代浏览器对 transformopacity 属性有极高的优化。修改这两个属性通常不会触发重排和重绘,而是会触发 合成 (Composite) 过程。浏览器会将该元素提升到一个独立的 合成层 (Compositing Layer),动画的每一帧仅由 GPU 来移动或改变图层的透明度,性能极高。

  • transform: translate() 代替 top/left 的修改。
  • transform: scale() 代替 width/height 的修改。
  • opacity 代替 visibility (如果只是想隐藏)。

4. 将元素脱离文档流

如果你需要对一个元素进行复杂的动画或频繁操作,可以将其 position 设置为 absolutefixed,使其脱离正常的文档流。这样,它的变化就不会影响到其他元素,重排的范围会被限制在这个元素自身。

5. 使用 will-change 属性

will-change 是一个 CSS 属性,可以提前告知浏览器某个元素将要发生变化,让浏览器有机会提前进行优化,比如提前创建合成层。

.moving-element {
will-change: transform, opacity;
}

注意:不要滥用 will-change,因为它会预先占用 GPU 资源。只在确实需要动画的元素上使用,并在动画结束后移除它。

总结

操作定义开销触发例子优化方向
重排 (Reflow)重新计算元素的几何位置和大小非常高改变 width, height, margin, position;获取 offsetWidth批量修改 DOM,读写分离,使用 transform
重绘 (Repaint)重新绘制元素的外观,位置不变中等改变 color, background-color, visibility减少不必要的外观改变
合成 (Composite)GPU 合并图层,直接显示非常低改变 transform, opacity首选,用此实现动画