浏览器中的重构与回流

原文:Browser | 重绘和回流
前言:回流(重排)和重绘是前端面试过程中经常会提到的一个知识点,所以想写一篇文章整理下相关的知识点。

要了解什么是回流(重排)与重绘,首先就要了解一下浏览器是如何进行画面渲染的。

浏览器是如何进行画面渲染的?

  • 解析(Parser)HTML,生成DOM树(Tree),解析(Parser)CSS,生成样式规则(Style Rules)
  • 根据刚才生成的DOM树与样式规则,生成渲染树(Render Tree)
  • 进行布局Layout(回流/重排):根据生成的渲染树,得到节点的集合信息(位置,大小)
  • 进行绘制Painting(重绘):根据计算和获取的信息进行整个页面的绘制
  • Display:最后将画面显示在页面上

重绘和回流(重排)

  • 回流:渲染树(Render Tree)中的元素的尺寸、结构、布局(几何属性)发生改变的时候,浏览器就会重新渲染部分或者全部文档的过程。
  • 重绘:对元素样式的改变并不影响它在文档流中的位置和文档布局(没有改变元素的几何属性),浏览器直接为该元素绘制新的样式。

回流的过程在重绘的过程前面,所以回流一定会重绘,但重绘不一定会引起回流

如何触发

回流(重排)

  • 页面一开始加载的时候(无法避免)

  • 脚本操作DOM(增加或者删除可见的DOM元素)

  • 元素的几何属性发生变化(大小,位置)

  • 元素的内容发生了变化(如:input框中输入的内容,图片被另一个不同尺寸的图片所替代)

  • 激活css伪类(如:#div::hover

  • 字体的大小发生改变

  • 浏览器的窗口大小发生了改变(回流是根据视口的大小来计算元素的位置和大小的)

  • 使用一些特定的属性

    offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight

    这些属性是通过实时计算得到的,浏览器获取这些值的时候,也会进行回流的操作,使用getComputedStyle方法的时候同理

大部分时候可以认为,只要影响到页面布局就会有回流的发生。

重绘

  • 上文提到的回流的过程在重绘的过程前面,所以回流一定会重绘,但重绘不一定会引起回流
  • 颜色的修改
  • 文本方向的修改
  • 阴影的修改

下面来看几个和重绘与回流相关的案例熟悉一下。

案例1:

javascript 复制代码
var s = document.body.style;
s.padding = "2px"; // 回流+重绘
s.border = "1px solid red"; // 再一次 回流+重绘
s.color = "blue"; // 再一次重绘
s.backgroundColor = "#ccc"; // 再一次 重绘
s.fontSize = "14px"; // 再一次 回流+重绘
// 添加node,再一次 回流+重绘
document.body.appendChild(document.createTextNode('abc!'));

案例2:

问题:display:nonevisibility:hidden会产生回流与重绘吗?

答:

  • display:none元素隐藏之后不占用文档流(在渲染树里面不存在节点),DOM树发生了变化,所以会引起重绘与回流。

  • visibility:hidden显示在页面上,但是隐藏元素仍需占用与未隐藏时一样的空间(在渲染树里面存在节点),没有影响到页面的结构,所以只有产生重绘。

补充:display: none 的子元素不会进行显示,而visibility: hidden的子元素却是可以进行设置显示的

浏览器优化机制

由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程

浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列.

当你获取布局信息的操作的时候,会强制队列刷新,包括前面讲到的offsetTop等方法都会返回最新的数据

因此浏览器不得不清空队列,触发回流重绘来返回正确的值。

如何避免触发回流与重绘

  1. 如果想通过js修改样式最好通过类(class)的方式去触发(补充:使用cssText也可以)
  2. 避免设置多项内联样式
  3. 批量修改dom时候通过以下思路减少回流与重绘的发生
    • 使元素脱离文档流
    • 对其进行多次修改
    • 将元素带回到文档中
  4. 避免触发同步布局事件,比如前文提到的需要实时读取的属性(如:offsetWidth),这样不需要每次循环的时候都读取一次
  5. 遇到复杂的动画效果,使用position: fixed/absolute让其脱离文档流,从而减少对其他元素的影响
  6. css3硬件加速,可以让transformopacityfilters这些动画不会引起回流重绘。
    • 如果你为太多元素使用css3硬件加速,会导致内存占用较大,会有性能问题。
    • 在GPU渲染字体会导致抗锯齿无效。这是因为GPU和CPU的算法不同。因此如果你不在动画结束的时候关闭硬件加速,会产生字体模糊。

设定元素样式,最好通过类(class)的方式去触发

下面这种方式,在比较老的浏览器上每次赋值操作都会引起回流与重绘

❕注:比较新的浏览器会使用队列来储存多次修改,进行优化,所以在新的浏览器上只会触发一次重绘和回流

javascript 复制代码
const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'

为了避免触发重回与回流,可以使用类(class)来给元素触发对应的样式,下面就是优化过后的代码:

CSS:

css 复制代码
.active {
  width: 100px;
  height: 200px;
  border: 10px solid red;
  color: red;
}

JS:

javascript 复制代码
const el = document.querySelector('#container');
// 给元素追加一个类
el.classList.add('active');

补充:其实还可以使用cssText来给样式做重新赋值,但是这种方式写法看上去比较繁琐,所以了解一下就好了。对应的代码如下:

javascript 复制代码
const el = document.querySelector('#container');
// 通过cssText属性给元素添加样式
el.style.cssText = 'width: 100px;' + 
                  'height: 200px;' +
                  'border: 10px solid red;' +
                  'color: red;';

批量修改dom时候通过以下思路减少回流与重绘的发生

  1. 使元素脱离文档流
  2. 对其进行多次修改
  3. 将元素带回到文档中

该过程的第一步和第三步可能会引起回流,但是经过第一步之后,对DOM的所有修改都不会引起回流,因为它已经不在渲染树了

有三种方式可以让DOM脱离文档流:

  • 隐藏元素,应用修改,重新显示
  • 使用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档。
  • 将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素。

隐藏元素,应用修改,重新显示

javascript 复制代码
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';

uldisplay设置为none后,该元素就不存在与渲染树中了

使用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档

javascript 复制代码
const ul = document.getElementById('list');
// 使用createDocumentFragment创建一个新的空白的文档片段
const fragment = document.createDocumentFragment();
appendDataToElement(fragment, data);
// 将元素追加到ul元素中去
ul.appendChild(fragment);

DocumentFragments (en-US) 是 DOM 节点。它们不是主 DOM 树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到 DOM 树。在 DOM 树中,文档片段被其所有的子元素所代替。

因为文档片段存在于内存中,并不在 DOM 树中 ,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)。因此,使用文档片段通常会带来更好的性能。------(MDN-Document.createDocumentFragment()

将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素。

javascript 复制代码
const ul = document.getElementById('list');
// 对该节点进行克隆,参数1为true代表深度克隆
const clone = ul.cloneNode(true);
appendDataToElement(clone, data);
// 将旧的元素替换为修改后的元素
ul.parentNode.replaceChild(clone, ul);

避免触发同步布局事件

javascript 复制代码
// 将变量放在循环的外面,避免每次循环的时候都要重新读取,进而导致没必要的重绘与回流发生
const width = box.offsetWidth;
for (let i = 0; i < paragraphs.length; i++) {
    paragraphs[i].style.width = width + 'px';
}

css3硬件加速(GPU加速)

划重点:使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘 。但是对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。

常见的触发硬件加速的css属性:

  • transform
  • opacity
  • filters
  • Will-change

注意:

  • 如果你为太多元素使用css3硬件加速,会导致内存占用较大,会有性能问题。
  • 在GPU渲染字体会导致抗锯齿无效。这是因为GPU和CPU的算法不同。因此如果你不在动画结束的时候关闭硬件加速,会产生字体模糊。

参考文献

相关推荐
LaughingZhu5 小时前
Product Hunt 每日热榜 | 2026-05-21
前端·人工智能·经验分享·chatgpt·html
怕浪猫5 小时前
Electron 开发实战(一):从零入门核心基础与环境搭建
前端·electron·ai编程
小鹏linux6 小时前
Ubuntu 22.04 部署开源免费具有精美现代web页面的Casdoor账号管理系统
linux·前端·ubuntu·开源·堡垒机
前端若水7 小时前
会话管理:创建、切换、删除对话历史
前端·人工智能·python·react.js
Bigger7 小时前
mini-cc:一个轻量级 AI 编程助手的诞生
前端·ai编程·claude
涵涵(互关)7 小时前
Naive-ui树型选择器只显示根节点
前端·ui·vue
BY组态7 小时前
Ricon组态系统最佳实践:从零开始构建物联网监控平台
前端·物联网·iot·web组态·组态
BY组态7 小时前
Ricon组态系统vs传统组态软件:为什么选择新一代Web组态平台
前端·物联网·iot·web组态·组态
SoaringHeart7 小时前
Flutter进阶:OverlayEntry 插入图层管理器 NOverlayZIndexManager
前端·flutter
放下华子我只抽RuiKe57 小时前
React 从入门到生产(四):自定义 Hook
前端·javascript·人工智能·深度学习·react.js·自然语言处理·前端框架