浏览器的回流与重绘(Reflow & Repaint)
Category(分类): Browser Status: 已更新
“回流”和“重绘”是前端性能文章里常见的术语。它们仍然有助于理解样式变化的成本,但现代浏览器的实际流程包含样式计算、布局、绘制、栅格化和合成,具体更新范围由引擎判断,不是每次都从 <html> 重新执行全部步骤。
一、先建立正确模型
一个简化的渲染路径是:
DOM / CSS / JavaScript
↓
样式计算(Recalculate Style)
↓
布局(Layout)
↓
绘制记录(Paint / Display List)
↓
图块栅格化(Raster)
↓
合成(Composite)
↓
屏幕

早期资料常说“DOM 和 CSSOM 合并生成 Render Tree”。这可以作为教学模型,但现代浏览器还会维护布局片段、属性树、绘制块和合成结构;不同浏览器的内部命名也不同。

一句话仍然有参考价值:
影响几何信息的更新通常需要 Layout,并可能带来 Paint;只影响外观的更新可能只需要 Paint;某些 transform/opacity 动画可能只需要 Composite。
其中“可能”非常重要。浏览器会根据内容、动画、图层、设备和资源情况优化或改变路径。
二、什么是回流(Reflow/Layout)
现代浏览器更常使用 Layout 表示布局计算;“Reflow”是历史和工程文章中常见的同义词。它指浏览器重新计算一个或多个布局盒子的尺寸和位置,使页面几何关系保持正确。
以下变化可能触发布局:
- 初次布局、窗口或视口尺寸变化;
- 增删或移动参与布局的节点;
- 改变
width、height、padding、margin、边框、定位和字体等几何相关样式; - 文本、图片、字体加载等导致内容尺寸变化;
- 影响祖先尺寸或后续兄弟位置的样式变化;
- 读取布局信息时,浏览器为了得到最新值而被迫同步完成待处理的样式或布局工作。
.card {
width: 320px;
padding: 16px;
margin-block: 12px;
}
一个元素改变宽度,可能只影响局部布局,也可能改变父容器尺寸并影响大量后续内容。布局范围取决于布局模式、尺寸约束、包含关系、字体和浏览器优化,不能只凭属性名称判断一定是全局或局部。
读取布局信息可能触发强制同步布局
常见的布局读取包括:
offsetWidth、offsetHeight、offsetTop、offsetLeft;clientWidth、clientHeight;scrollWidth、scrollHeight、某些场景下的scrollTop/scrollLeft;getBoundingClientRect();- 读取某些属性时使用的
getComputedStyle()。
但“调用 getComputedStyle() 必然回流”也不准确。它至少可能触发样式计算;是否需要布局取决于浏览器、文档状态、媒体条件以及读取的属性。对已经清洁且不需要布局的数据读取,浏览器可能不必重新布局。
scrollTo()、scrollIntoView() 是改变滚动状态的操作,不应和所有布局读取混为一谈;它们是否引起额外样式、布局或绘制工作,需要结合具体页面和 trace 判断。
三、什么是重绘(Repaint/Paint)
当元素几何位置不变,但视觉外观变化时,浏览器可能重新生成绘制指令,例如:
color、background-color、背景图片;- 边框、轮廓、圆角、阴影;
- 文本、图片或其他替换内容的视觉变化;
- 某些
visibility、filter、混合和裁剪变化。

重绘之后通常还需要栅格化受影响的区域,最后可能重新合成。因此“只重绘就一定很便宜”也不成立:大面积阴影、复杂滤镜、高 DPR 图片和大量透明内容都可能让 Paint/Raster 成为瓶颈。
display: none 和 visibility: hidden
display: none的内容通常不参加布局,也不会绘制;把它切换回来需要重新参与样式和布局;visibility: hidden通常仍占据布局空间,只是不显示内容;切换它通常不需要改变几何位置,但仍可能需要样式和绘制更新;- 两者的选择还涉及可访问性、焦点、交互和布局占位,不能简单说某一个永远更优。
四、合成和 GPU
当浏览器已经有了绘制结果,且变化可以由合成器处理时,可能只更新层的变换、透明度、滚动偏移等属性。例如:
.avatar {
transition: transform 200ms ease, opacity 200ms ease;
}
.avatar.is-active {
transform: translateX(12px);
opacity: 0.8;
}
这类动画经常比逐帧修改 top、left 更容易避免布局,但不能保证所有 transform 都只走合成。元素内容变化、复杂 filter、阴影、混合、图层内存压力以及浏览器实现都可能重新绘制。

“GPU 加速”也不是一个前端 API 开关:
- 样式计算、JavaScript、布局和很多 Paint 工作仍然可能在 CPU/主线程发生;
- Raster 和 Composite 可能使用 GPU,也可能因为平台、驱动或内容回退;
- 合成层越多不一定越快,过大的纹理会增加内存和上传成本。
不要为了“强制硬件加速”到处使用 translateZ(0) 或 translate3d(0, 0, 0)。这类历史技巧可能在特定旧设备上有效,也可能造成层爆炸和内存浪费。
五、浏览器什么时候真正执行这些工作
浏览器会尽量合并连续的样式写入,在合适的渲染机会统一处理,而不是每一行 JavaScript 都立即 Layout/Paint:
const box = document.querySelector('.box')
box?.classList.add('expanded')
box?.classList.add('highlighted')
// 浏览器通常可以把连续写入合并到后续渲染机会
但如果在写入后立刻读取布局信息,浏览器为了返回准确结果可能被迫提前刷新:
const box = document.querySelector('.box')
box?.classList.add('expanded')
const width = box?.getBoundingClientRect().width
console.log(width)
这不代表所有读写交错都会固定触发某个次数的回流,但大量交错读写容易形成 layout thrashing(布局抖动)。
读写分离
const items = [...document.querySelectorAll('.item')]
// 先集中读取
const positions = items.map(item => item.getBoundingClientRect().left)
// 再集中写入
items.forEach((item, index) => {
item.style.transform = `translateX(${positions[index] + 8}px)`
})
如果数据来自用户输入、滚动或窗口变化,可以在 requestAnimationFrame 中批量处理视觉写入:
let scheduled = false
let latestX = 0
window.addEventListener('pointermove', event => {
latestX = event.clientX
if (scheduled) return
scheduled = true
requestAnimationFrame(() => {
scheduled = false
document.documentElement.style.setProperty('--pointer-x', `${latestX}px`)
})
})
requestAnimationFrame 表示在下一次浏览器绘制前请求回调,不是“强制每秒 60 帧”,也不是调用一次就保证一定产生新帧。
六、减少影响范围的方式
1. 批量修改 class
const panel = document.querySelector('.panel')
panel?.classList.toggle('is-open', true)
相比到处修改内联属性,使用 class 更容易维护,也便于浏览器一次计算相关样式。并不是说 class 永远比 style 快,关键在于更新次数、选择器复杂度和实际影响范围。
2. 使用 DocumentFragment 或离线节点
对尚未插入文档的节点批量构建,可以减少中间状态参与布局:
const list = document.querySelector('.list')
const fragment = document.createDocumentFragment()
for (const name of ['A', 'B', 'C']) {
const item = document.createElement('li')
item.textContent = name
fragment.append(item)
}
list?.append(fragment)
DocumentFragment 不是性能魔法;如果最终只是一次 append,现代浏览器已经会优化很多场景。它的主要价值是让中间节点保持脱离文档,并使代码结构清晰。
3. 暂时隐藏或替换节点
先设置 display: none、在副本上修改,再一次性替换,可能减少中间布局;但隐藏和重新显示本身也会产生样式/布局成本,还可能影响焦点、动画和可访问性,应根据交互场景测量。
4. CSS containment 和 content-visibility
对于独立的长列表或折叠区域,可以评估:
.article-section {
contain: layout paint;
}
.long-list {
content-visibility: auto;
contain-intrinsic-size: auto 800px;
}
contain 会改变布局、绘制或尺寸传播边界,content-visibility: auto 可能跳过视口外内容的渲染工作。使用前要验证滚动高度、焦点、搜索、可访问性和内容尺寸占位。
七、优化动画和事件
- 优先让动画只改变确实适合合成的
transform、opacity; - 需要布局的动画可让元素使用合适的
position,降低对其他流式内容的影响,但绝对定位不是万能优化; will-change只在确实需要且时间有限时使用,动画结束后清理;scroll、resize、pointermove等高频事件应批处理,并避免在每次回调中强制读取布局;- 不要为了减少回流而牺牲语义、焦点、响应式布局和可维护性。
八、图片懒加载
原文使用 scrollTop、offsetTop 轮询,这在理解原理时有价值,但现代页面可以优先使用浏览器提示:
<img
src="placeholder.webp"
data-src="large-photo.webp"
alt="风景"
width="1200"
height="800"
loading="lazy"
decoding="async"
>
需要自定义占位、预加载边界或兼容特殊资源时,可以使用 IntersectionObserver:
const images = document.querySelectorAll('img[data-src]')
const observer = new IntersectionObserver((entries, currentObserver) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue
const image = entry.target
const source = image.dataset.src
if (source) image.src = source
image.removeAttribute('data-src')
currentObserver.unobserve(image)
}
}, {
rootMargin: '200px 0px'
})
images.forEach(image => observer.observe(image))
不要为首屏关键图片盲目设置 loading="lazy";这可能延迟 LCP。图片还应提供尺寸,避免加载完成后改变布局造成 CLS。
九、用 Performance 面板验证
Chrome DevTools 的 Performance 录制可以观察:
- JavaScript 和 Long Task;
Recalculate Style、Layout、Paint;- Raster、Composite、帧时间和输入延迟;
- Layout Shift、LCP 和资源加载。


面板颜色和事件名称会随 DevTools 版本变化,不能简单说“蓝色一定是网络、紫色一定是回流、绿色一定是重绘”。应点击具体事件查看耗时、调用栈和影响节点。

十、结论
- Layout/Reflow 关注几何信息;Paint/Repaint 关注视觉绘制;Raster 和 Composite 是后续独立阶段;
- 回流通常可能带来重绘,但具体更新范围由浏览器决定;重绘也可能因为大面积复杂内容而很昂贵;
- 布局读取不一定每次强制布局,但读写交错容易造成同步刷新;
transform、opacity、will-change和 GPU 不是万能性能开关;- 先用 Performance、LCP、INP、CLS 和真实用户数据定位,再选择批量更新、隔离、懒加载或动画策略。