前端的重绘重排
网页生成过程
网页的生成是一个复杂的过程,主要包括以下几个阶段:
- HTML解析阶段:
- 浏览器逐行解析HTML文档。
- 构建DOM(Document Object Model)树。
- 遇到标签会暂停解析,执行脚本(除非使用async/defer)。
- CSS解析阶段:
- 解析外部CSS文件和内联样式。
- 构建CSSOM(CSS Object Model)树。
- 解析过程是递归的(从右向左选择器匹配)。
- 渲染树构建:
- 合并DOM树和CSSOM树。
- 只包含需要显示的节点(如不包含display:none的元素)。
- 计算每个节点的CSS属性值。
- 布局阶段(Layout/Reflow):
- 计算每个节点在视口中的确切位置和大小。
- 从根节点开始递归计算。
- 生成"盒模型"精确信息。
- 绘制阶段(Paint):
- 将布局计算的几何信息转换为屏幕上的实际像素。
- 通常分为多个图层(Composite Layers)进行绘制。
- 最后进行图层合成(Compositing)。
网页生成示例 /* 定义一个盒子样式 */ .box { width: 200px; height: 200px; background-color: lightblue; margin: 20px; } // 打印信息,表明DOM内容已加载 console.log('DOMContentLoaded'); // 获取元素并修改其样式 const box = document.querySelector('.box'); box.style.border = '2px solid red';
重绘(Repaint)
重绘是指视觉外观改变但是几何位置不变的时候,浏览器需要重新绘制元素外观的过程。
举例
颜色、背景、阴影、文字内容等不会影响元素布局的内容的改变会触发重绘。
触发条件
- 修改元素的 color、background-color、border-style、box-shadow 等样式属性。
- 修改 visibility(不影响布局,但会改变视觉状态)。
- 修改文本内容(如 textContent)。
- 滚动条样式的变化(如 scrollbar-width)。
改变颜色触发重绘
改变颜色改变背景触发重绘改变背景 改变文本内容触发重绘 改变文本 function changeColor() { const p = document.getElementById('color-change'); p.style.color = 'blue'; } function changeBackground() { const div = document.getElementById('bg-change'); div.style.backgroundColor = 'orange'; } function changeText() { const span = document.getElementById('text-change'); span.textContent = '文本已改变'; }重排(Reflow)
重排是指几何位置或者尺寸发生改变的时候就会触发重排,更新页面布局的行为。
举例
元素宽度、高度、边距、定位、添加或删除DOM节点等操作会触发重排。
触发条件
- 几何属性修改:width、height、padding、margin、border-width、top、left、transform(2D/3D 转换会触发合成,但若涉及布局变化也会触发重排)等。
- 布局相关操作:
- 添加或删除可见的 DOM 节点、修改 display(如 none 会触发重排,visibility: hidden 仅触发重绘)、flex/grid 布局的变化。
- 获取布局信息(浏览器会强制同步布局计算):offsetWidth、offsetHeight、offsetTop、offsetLeft、scrollWidth、scrollHeight、clientWidth、clientHeight、getBoundingClientRect() 等属性或方法的调用。
特点
重排成本很高,会影响整个界面布局,导致父子元素连锁变化。浏览器尽可能批量处理重排,但是频繁重排会导致性能瓶颈。
以下是多个触发重排的代码示例:
改变宽度触发重排改变宽度添加节点触发重排添加节点修改display触发重排修改display function changeWidth() { const div = document.getElementById('width-change'); div.style.width = '200px'; } function addNode() { const div = document.getElementById('node-add'); const newDiv = document.createElement('div'); newDiv.textContent = '新添加的节点'; div.appendChild(newDiv); } function changeDisplay() { const div = document.getElementById('display-change'); div.style.display = 'none'; }重绘与重排的关系
重排通常伴随着重绘,但是重绘不一定有重排。
性能优化
为了提高网页性能,我们需要尽量减少重绘和重排的次数。以下是一些性能优化的方法:
- 减少重排重绘次数
- 批量修改:
// 不好的写法:多次修改样式(触发多次重排) const element = document.createElement('div'); element.style.width = '100px'; element.style.height = '200px'; element.style.backgroundColor = 'red'; // 好的写法:通过 class 一次性修改 const newElement = document.createElement('div'); newElement.className = 'new-style';
.new-style { width: 100px; height: 200px; background-color: red; }
- **使用文档碎片**:
批量操作 DOM 时,先将节点添加到 DocumentFragment 中,再一次性插入 DOM 树,避免多次触发重排。
const fragment = document.createDocumentFragment(); for (let i = 0; i
- 避免频繁的获取布局信息
- 缓存布局信息:如需多次读取元素的布局属性(如 offsetWidth),先将其值缓存到变量中,避免重复触发重排。
// 不好的写法:多次读取 offsetWidth(触发多次重排) const badElement = document.createElement('div'); for (let i = 0; i
- 利用层叠上下文(CSS 3D/硬文件加速)
将频繁动画的元素脱离文档流,使用 will-change 或 transform: translateZ(0) 使其创建层叠上下文,让浏览器为其单独分配图层,避免影响其他元素的布局。
.animated-element { width: 100px; height: 100px; background-color: lightblue; will-change: transform; transform: translateZ(0); animation: move 2s infinite; } @keyframes move { from { transform: translateX(0); } to { transform: translateX(200px); } }
- 合理使用display:none
对需要频繁操作的元素,先设置 display: none(使其脱离文档流,重排成本降低),操作完成后再显示。
const element = document.createElement('div'); element.style.display = 'none'; // 触发一次重排 // 模拟执行大量 DOM 修改 for (let i = 0; i
- 使用 CSS 动画替代 JavaScript 动画
CSS 动画(如 transition、animation)由浏览器优化处理,尽量避免通过 JavaScript 频繁修改样式触发重排。
.css-animation { width: 100px; height: 100px; background-color: lightgreen; transition: width 1s; } .css-animation:hover { width: 200px; }
- 集中修改样式
不好的写法 好的写法 .new-style { left: 10px; top: 200px; transform: scale(1.1); } const box = document.getElementById('box'); function badChange() { box.style.left = '10px'; box.style.top = '200px'; box.style.transform = 'scale(1.1)'; } function goodChange() { box.classList.add('new-style'); }
- 不使用table布局
table中任何元素变化都会导致整个表格重排,因此尽量避免使用。以下是一个简单的表格示例及对比:
改变表格单元格1 单元格2 单元格1单元格2- 离线修改DOM
- DOM设置display: none;,将其从渲染树中移除,然后再进行复杂修改操作,最后再将其显示出来。整个过程包含隐藏和显示共两次重排。
- 或者使用DocumentFragment创建一个DOM碎片,然后在其上面批量操作DOM,操作完成之后再添加到文档中,这样只会触发一次重排。
// 方法一:使用 display: none const element = document.createElement('div'); document.body.appendChild(element); element.style.display = 'none'; for (let i = 0; i
- 分离读写操作
不好的读写操作 好的读写操作 const div = document.getElementById('div'); function badReadWrite() { div.style.left = '10px'; div.style.top = '10px'; div.style.width = '20px'; div.style.height = '20px'; console.log(div.offsetLeft); console.log(div.offsetTop); console.log(div.offsetWidth); console.log(div.offsetHeight); } function goodReadWrite() { const width = div.offsetWidth; const height = div.offsetHeight; div.style.width = (width + 10) + 'px'; div.style.height = (height + 10) + 'px'; console.log(width); console.log(height); }
"统一读,再统一写"的优化原理,实际上是浏览器渲染机制中的**布局抖动(Layout Thrashing)**问题的解决方案。以下是详细解释:
(图片来源网络,侵删)核心机制解析
- 浏览器渲染队列优化:
现代浏览器会维护一个"写操作队列",将连续的样式修改操作批量处理。这种批处理可以避免每次样式修改都立即触发重排。
- 强制同步布局(Forced Synchronous Layout):
当JavaScript代码读取布局属性(如offsetWidth、clientHeight等)时,浏览器必须立即计算最新布局以保证返回准确值,这会强制刷新并执行当前队列中的所有待处理写操作。
- 典型问题场景:
// 反模式:读写交替 const element = document.createElement('div'); element.style.width = '100px'; // 写 - 加入队列 let w1 = element.offsetWidth; // 读 - 强制刷新队列(触发重排) element.style.height = '200px'; // 写 - 加入队列 let h1 = element.offsetHeight; // 读 - 再次强制刷新队列(触发重排)
这样会导致多次不必要的重排。
(图片来源网络,侵删)统一读写法示例
const element = document.createElement('div'); // 统一读写法示例 const width = element.offsetWidth; // 第一次读取(触发1次重排) console.log(width); // 仅仅是日志输出,不涉及布局读取 console.log(width); // 同上
- 第一次读取:
element.offsetWidth强制浏览器执行队列中的所有待处理操作,触发1次完整的重排计算,并将结果缓存到width变量。
- 后续console.log:
只是输出已缓存的变量值,不涉及任何布局属性读取,不会触发额外的重排。
为什么不会多次触发重排?
关键区别在于:
(图片来源网络,侵删)- 读取布局属性:offsetWidth等会强制重排。
- 普通变量读取:已缓存的变量不会触发重排。
浏览器的工作流程:
[初始状态] 写队列: [] 1. 读取offsetWidth: - 发现需要最新布局信息 - 立即执行: 刷新写队列 → 计算布局 → 返回结果 - 重排计数 +1 2. 后续console.log: - 只是JavaScript执行环境中的变量访问 - 不涉及渲染引擎 - 不会触发重排
实际开发中的正确实践
const element = document.createElement('div'); // 1. 先批量读取所有需要的布局信息 const width = element.offsetWidth; const height = element.offsetHeight; // 2. 然后执行批量样式修改 element.style.width = (width + 10) + 'px'; element.style.height = (height + 10) + 'px'; // 整个过程只触发1次初始读取时的重排
特殊情况的注意事项
即使统一读取,某些情况下仍可能触发多次重排:
const element1 = document.createElement('div'); const element2 = document.createElement('div'); const width1 = element1.offsetWidth; // 重排1 const width2 = element2.offsetWidth; // 可能再次重排
因为不同元素的布局计算可能有依赖关系,浏览器可能需要多次计算。最安全的做法是使用requestAnimationFrame来分离读写操作。
这种优化原理正是Facebook的React等框架实现虚拟DOM(Virtual DOM)批处理更新的核心机制之一。
- 减少重排重绘次数