浏览器解析渲染机制
- 解析HTML,生成DOM树
- 解析CSS,生成CSSOM树
- 将DOM树和CSSOM树结合,生成渲染树(RenderTree)
- Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
- Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
- Display:将像素发送给GPU,展示在页面上
在页面初始渲染阶段,回流不可避免的触发,可以理解成页面一开始是空白的元素,后面添加了新的元素使页面布局发生改变 当我们对 DOM 的修改引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性,然后再将计算的结果绘制出来当我们对 DOM 的修改导致了样式的变化(color 或 background-color ),却并未影响其几何属性时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式,这里就仅仅触发了重绘
什么是回流重绘?
回流
回流(Layout)(也称为布局重排)是指浏览器根据网页内容计算并确定网页布局、尺寸和位置构建渲染树(Render Tree)过程。回流通常发生在以下情况:
- 添加、删除或修改DOM元素
- 改变DOM元素的尺寸、位置(如设置
width、height、padding、border、margin) - 改变视口(用户调整窗口大小或滚动页面)
- 页面初始渲染
- 激活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); // 直接添加到文档中