技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTBR

浏览器的回流与重绘(Reflow & Repaint)

所属馆藏
Browser
文件格式
Markdown
原始路径
Browser/33-浏览器的回流与重绘 (Reflow & Repaint)
本文目录11 个章节
  1. 一、先建立正确模型
  2. 二、什么是回流(Reflow/Layout)
  3. 三、什么是重绘(Repaint/Paint)
  4. 四、合成和 GPU
  5. 五、浏览器什么时候真正执行这些工作
  6. 六、减少影响范围的方式
  7. 七、优化动画和事件
  8. 八、图片懒加载
  9. 九、用 Performance 面板验证
  10. 十、结论
  11. 参考资料

浏览器的回流与重绘(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”是历史和工程文章中常见的同义词。它指浏览器重新计算一个或多个布局盒子的尺寸和位置,使页面几何关系保持正确。

以下变化可能触发布局:

  • 初次布局、窗口或视口尺寸变化;
  • 增删或移动参与布局的节点;
  • 改变 widthheightpaddingmargin、边框、定位和字体等几何相关样式;
  • 文本、图片、字体加载等导致内容尺寸变化;
  • 影响祖先尺寸或后续兄弟位置的样式变化;
  • 读取布局信息时,浏览器为了得到最新值而被迫同步完成待处理的样式或布局工作。
.card {
  width: 320px;
  padding: 16px;
  margin-block: 12px;
}

一个元素改变宽度,可能只影响局部布局,也可能改变父容器尺寸并影响大量后续内容。布局范围取决于布局模式、尺寸约束、包含关系、字体和浏览器优化,不能只凭属性名称判断一定是全局或局部。

读取布局信息可能触发强制同步布局

常见的布局读取包括:

  • offsetWidthoffsetHeightoffsetTopoffsetLeft
  • clientWidthclientHeight
  • scrollWidthscrollHeight、某些场景下的 scrollTop/scrollLeft
  • getBoundingClientRect()
  • 读取某些属性时使用的 getComputedStyle()

但“调用 getComputedStyle() 必然回流”也不准确。它至少可能触发样式计算;是否需要布局取决于浏览器、文档状态、媒体条件以及读取的属性。对已经清洁且不需要布局的数据读取,浏览器可能不必重新布局。

scrollTo()scrollIntoView() 是改变滚动状态的操作,不应和所有布局读取混为一谈;它们是否引起额外样式、布局或绘制工作,需要结合具体页面和 trace 判断。

三、什么是重绘(Repaint/Paint)

当元素几何位置不变,但视觉外观变化时,浏览器可能重新生成绘制指令,例如:

  • colorbackground-color、背景图片;
  • 边框、轮廓、圆角、阴影;
  • 文本、图片或其他替换内容的视觉变化;
  • 某些 visibilityfilter、混合和裁剪变化。

三种常见渲染路径示意

重绘之后通常还需要栅格化受影响的区域,最后可能重新合成。因此“只重绘就一定很便宜”也不成立:大面积阴影、复杂滤镜、高 DPR 图片和大量透明内容都可能让 Paint/Raster 成为瓶颈。

display: nonevisibility: hidden

  • display: none 的内容通常不参加布局,也不会绘制;把它切换回来需要重新参与样式和布局;
  • visibility: hidden 通常仍占据布局空间,只是不显示内容;切换它通常不需要改变几何位置,但仍可能需要样式和绘制更新;
  • 两者的选择还涉及可访问性、焦点、交互和布局占位,不能简单说某一个永远更优。

四、合成和 GPU

当浏览器已经有了绘制结果,且变化可以由合成器处理时,可能只更新层的变换、透明度、滚动偏移等属性。例如:

.avatar {
  transition: transform 200ms ease, opacity 200ms ease;
}

.avatar.is-active {
  transform: translateX(12px);
  opacity: 0.8;
}

这类动画经常比逐帧修改 topleft 更容易避免布局,但不能保证所有 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 可能跳过视口外内容的渲染工作。使用前要验证滚动高度、焦点、搜索、可访问性和内容尺寸占位。

七、优化动画和事件

  • 优先让动画只改变确实适合合成的 transformopacity
  • 需要布局的动画可让元素使用合适的 position,降低对其他流式内容的影响,但绝对定位不是万能优化;
  • will-change 只在确实需要且时间有限时使用,动画结束后清理;
  • scrollresizepointermove 等高频事件应批处理,并避免在每次回调中强制读取布局;
  • 不要为了减少回流而牺牲语义、焦点、响应式布局和可维护性。

八、图片懒加载

原文使用 scrollTopoffsetTop 轮询,这在理解原理时有价值,但现代页面可以优先使用浏览器提示:

<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 StyleLayoutPaint
  • Raster、Composite、帧时间和输入延迟;
  • Layout Shift、LCP 和资源加载。

Performance 时间线示意

动画任务拆解示意

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

合成动画与主线程更新对比

十、结论

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

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS