浏览器渲染的基本流程
要理解重排和重绘,首先需要了解浏览器渲染页面的基本步骤:
- 解析 (Parse):浏览器解析 HTML 生成 DOM 树 (DOM Tree),解析 CSS 生成 CSSOM 树 (CSSOM Tree)。
- 渲染树 (Render Tree):将 DOM 树和 CSSOM 树结合起来,生成渲染树 (Render Tree)。渲染树只包含需要显示的节点和它们的样式信息(比如
display: none的节点不会在渲染树中)。 - 布局 (Layout/Reflow):浏览器根据渲染树计算出每个节点在屏幕上的确切位置和大小。这个过程就是 布局,也常被称为 重排 (Reflow)。
- 绘制 (Paint/Repaint):浏览器根据计算好的布局信息,将每个节点绘制到屏幕上(比如颜色、边框、阴影等)。这个过程就是 绘制,也常被称为 重绘 (Repaint)。
- 合成 (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,offsetHeightscrollTop,scrollLeft,scrollWidth,scrollHeightclientTop,clientLeft,clientWidth,clientHeightgetComputedStyle()
二、重绘 (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 统一修改
// JSel.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. 使用 transform 和 opacity 实现动画
现代浏览器对 transform 和 opacity 属性有极高的优化。修改这两个属性通常不会触发重排和重绘,而是会触发 合成 (Composite) 过程。浏览器会将该元素提升到一个独立的 合成层 (Compositing Layer),动画的每一帧仅由 GPU 来移动或改变图层的透明度,性能极高。
- 用
transform: translate()代替top/left的修改。 - 用
transform: scale()代替width/height的修改。 - 用
opacity代替visibility(如果只是想隐藏)。
4. 将元素脱离文档流
如果你需要对一个元素进行复杂的动画或频繁操作,可以将其 position 设置为 absolute 或 fixed,使其脱离正常的文档流。这样,它的变化就不会影响到其他元素,重排的范围会被限制在这个元素自身。
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 | 首选,用此实现动画 |