浏览器的渲染管线、回流与渲染(重绘)通俗入门到进阶
搞清楚“浏览器是怎么把 HTML/CSS/JS 变成屏幕上的像素”的全过程,弄明白“回流(Reflow)”、“重绘(Repaint)”、“合成(Composite)”这些关键词到底指什么,以及如何写出更流畅的页面与动画。
1. 一个直观比喻
- 把浏览器想象成一本杂志排版系统:
- 作者把文章(HTML)、样式规范(CSS)、脚本(JS)交给排版员(浏览器主线程)。
- 排版员先理解文章结构(DOM),再理解样式(CSSOM),合并成“要如何呈现每个元素”的清单(渲染树)。
- 决定每一段文字、图片的大小和位置(布局/回流)。
- 把内容涂上颜色、边框、阴影(绘制/重绘)。
- 最后把这些页面层层叠好(合成),送上印刷机(GPU)输出为像素。
2. 全流程总览:从源码到像素(Critical Rendering Path)
下面是现代浏览器把页面绘制出来的大致流程(简化版):
网络请求/缓存
↓
解析 HTML → 构建 DOM
↓ 解析 CSS → 构建 CSSOM
└─────────────── 合并(样式计算:Style Recalc)
↓
渲染树(Render Tree)
↓
布局(Layout / 回流 Reflow)
↓
分层(Layerization)与分块
↓
绘制(Paint / Raster)
↓
合成(Composite)
↓
屏幕显示(GPU)
- DOM(Document Object Model):HTML 的结构化表示(节点树)。
- CSSOM(CSS Object Model):CSS 的结构化表示(规则树)。
- 渲染树(Render Tree):DOM 中可见节点 + 计算后的样式(不包含 display: none),用于后续排版与绘制。
- 布局(Layout/回流 Reflow):计算每个盒子的尺寸与位置。
- 绘制(Paint):把颜色、文字、边框、阴影等画到位图(Raster)。
- 合成(Composite):把多个图层用 GPU 叠在一起,得到最终帧。
JavaScript 的位置:
- JS 可以修改 DOM 或样式,从而触发样式计算、布局或绘制。
- 同步脚本可能阻塞 HTML 解析(除非使用
defer / async)。 - 尽量减少在不合适的时机做大量 DOM 操作,避免卡顿。
3. 回流(Reflow / Layout)是什么
- 定义:回流是浏览器计算或重新计算页面元素几何信息(尺寸、位置、行内排版等)的过程。它也常被叫做“布局(Layout)”或“重排(Reflow/Relayout)”。
- 为什么开销大:因为一个元素的位置/尺寸变化,可能会影响同级、父级、后续元素的排版,浏览器需要重新计算受影响的那一部分(甚至整棵文档),成本较高。
- 回流的范围:
- 增量回流:只对受影响的子树/区域重新布局(现代浏览器会尽量优化)。
- 全局回流:某些操作会导致全页面重新布局(如改变根字体、窗口尺寸)。
- 常见触发回流的操作:
- DOM 结构变化:添加/删除节点、移动节点。
- 改变布局相关属性:
width/height/margin/padding/top/left/border/position/display/font-size/line-height/... - 更换字体、改变文字内容(影响行高、换行)。
- 窗口尺寸变化、滚动容器尺寸改变。
- 读取布局信息并迫使浏览器“立刻给出最新值”(强制同步布局),例如在样式变更后马上读取以下属性/方法:
offsetWidth/offsetHeight/offsetTop/offsetLeftclientWidth/clientHeightscrollWidth/scrollHeight/scrollTop/scrollLeftgetBoundingClientRect()getComputedStyle(...)的某些尺寸值
- 注意:读取本身并不总是回流,只有在浏览器有“未结算的布局变更”时,这些读取会强制它先做一次布局,以返回准确数值,这就叫“强制同步回流(Forced Reflow/Layout Thrashing)”。
4. 重绘(Repaint)与合成(Composite)
-
重绘(Repaint / Paint):
- 改变了元素的“外观”但没有改变其几何尺寸和排版关系时,浏览器只需重新绘制受影响区域。例如:
color、background-color、visibility、box-shadow、outline(大多情况)等。
- 重绘比回流成本低,但仍然需要 CPU/GPU 绘制操作。
- 改变了元素的“外观”但没有改变其几何尺寸和排版关系时,浏览器只需重新绘制受影响区域。例如:
-
合成(Composite):
- 一些变化可以由“合成线程/GPU”直接处理,而无需回流和重绘。例如只改变:
transform(如translate/scale/rotate)opacity
- 这些属性可以在“合成阶段”完成,仅更新图层的变换或透明度,非常高效,适合做动画。
- 注意:不是所有
transform/opacity都100%不触发重绘,具体实现细节与是否独立图层有关。通常搭配will-change: transform, opacity或让元素成为合成层,才能稳定享受合成路径的性能。
- 一些变化可以由“合成线程/GPU”直接处理,而无需回流和重绘。例如只改变:
小结(经验规则,非绝对):
- 影响布局 → 回流(最贵)
- 不影响布局但需重新画像素 → 重绘(中等)
- 仅图层合成变化 → 合成(最省)
5. “渲染”一词的两个层面
- 广义“渲染”:指整个从解析到最终呈现的流程(渲染管线)。
- 狭义“渲染/绘制”:指把样式画到位图的阶段(Paint),以及合成阶段(Composite)。
在日常交流中,“渲染”有时指“绘制”,有时指“整个过程”,语境不同要留意。
6. 性能坑与优化实践
6.1 避免读写交错导致的强制同步布局(Layout Thrashing)
坏示例:在循环里交替“读布局”再“写样式”,会逼迫浏览器每次都先回流,性能很差。
const items = document.querySelectorAll('.item');
for (const el of items) {
// 写:改变样式(可能令布局脏)
el.style.width = (el.offsetWidth + 10) + 'px'; // 这里读了 offsetWidth,且在写之后读取,强制同步布局
}
改进:先批量“读”,再批量“写”,或者把写聚合到 requestAnimationFrame。
const items = [...document.querySelectorAll('.item')];
// 批量读(不会让中间产生写)
const widths = items.map(el => el.offsetWidth);
// 批量写
requestAnimationFrame(() => {
items.forEach((el, i) => {
el.style.width = (widths[i] + 10) + 'px';
});
});
更进一步:通过切换 class 一次性应用多条样式,减少逐条 style.xxx 的写入。
6.2 动画尽量用合成友好属性
- 优先用
transform和opacity做动画,避免频繁改top/left/width/height。 - 适当使用:
will-change: transform, opacity;提示浏览器提前为该元素分配合成层和资源(慎用,别全站到处加)。- 或在动画开始前设置一次
transform: translateZ(0)促成独立图层(旧技巧,注意内存和层过多的副作用)。
示例:使用 transform 做位移动画
.box {
transition: transform 300ms ease;
}
.box.move {
transform: translateX(200px);
}
6.3 少量且成批地修改 DOM
- 批量插入:使用
DocumentFragment或一次性innerHTML拼装好再塞进去。 - 多次样式改动:尽量合并到一个类或使用
el.style.cssText批量写入。 - 避免频繁触发回流的属性修改,必要时在 off-screen 容器修改(如
position: absolute; left:-9999px;),完成后再放回。
6.4 巧用 display 与 visibility
display: none:元素不参与布局,切换到block等会引发回流。visibility: hidden:元素保留占位但不可见,通常触发重绘,不引发布局变化。- 根据场景选择:如果只是临时隐藏,且频繁切换,可优先考虑
visibility或opacity(配合合成层)。
6.5 避免在热点路径读取会触发布局的属性
以下属性/方法在布局脏时读取会触发强制同步布局:
offsetWidth/offsetHeight/offsetTop/offsetLeftclientWidth/clientHeightscrollWidth/scrollHeight/scrollTop/scrollLeftgetBoundingClientRect()getComputedStyle(el).width/height/...
建议:
- 在事件回调中(如
scroll、resize)做节流或防抖。 - 先集中读取,再集中写入。
- 把昂贵操作移到
requestAnimationFrame或requestIdleCallback。
6.6 CSS Containment 与 content-visibility
-
contain:限制元素对子树之外的影响,缩小回流/重绘范围。例如:.card { contain: layout paint size style; } -
content-visibility: auto:允许浏览器跳过不可见内容的布局与绘制,显著优化长列表初次渲染。.list-item { content-visibility: auto; contain-intrinsic-size: 200px; /* 预估尺寸,避免布局跳动 */ }
6.7 长列表优化
- 虚拟列表/窗口化(只渲染视口附近的元素)。
IntersectionObserver延迟加载图片与模块。- 图片用合适尺寸与
loading="lazy",CSS 使用aspect-ratio预留空间,减少布局抖动(CLS)。
6.8 减少渲染阻塞与首屏关键路径
- CSS 阻塞首次渲染:尽量合并关键 CSS,避免多层
@import,关键样式内联。 - JS 阻塞解析:给非关键脚本加
defer或async。 - 资源提示:
<link rel="preload">、preconnect、dns-prefetch预热关键资源。 - 字体优化:
font-display: swap减少 FOIT;子集化字体减小体积;预加载关键字体。
6.9 把重活移出主线程
- 使用 Web Worker 处理计算密集任务,避免阻塞渲染。
- Canvas 绘制用
OffscreenCanvas放入 Worker(受支持的环境)。 - 大量图片处理、数据解析优先在 Worker。
6.10 合成层数量要适度
- 过多的图层会增加内存占用与合成开销。
- 只为要做动画或滚动的关键元素创建合成层,谨慎使用
will-change/translateZ(0)。 - 在 DevTools 的 Layers/Performance 面板查看层数量与合成开销。
7. 每帧预算与浏览器帧循环
- 60 FPS → 每帧约 16.67ms;120 Hz 屏幕 → 每帧约 8.33ms。
- 一帧内需要完成:事件处理 → JS 执行 → 样式计算 → 布局 → 绘制 → 合成。
- 动画与滚动流畅的关键,是让每帧内的总工作量别超过预算。把易抖动的工作放到合成阶段(transform/opacity)最稳妥。
requestAnimationFrame 用法示例(在下一帧绘制前批量提交写操作):
let pending = false;
const updates = [];
function scheduleUpdate(fn) {
updates.push(fn);
if (!pending) {
pending = true;
requestAnimationFrame(() => {
// 批量写入,避免多次交错
updates.forEach(u => u());
updates.length = 0;
pending = false;
});
}
}
// 使用
scheduleUpdate(() => {
box.style.transform = 'translateX(100px)';
});
8. 开发者工具:测量胜于猜测
- Chrome DevTools → Performance 面板:
- 录制一段交互,查看 Main/Compositor 线程,定位 Style/Layout/Paint/Composite 的耗时。
- Rendering 面板(Rendering 标签):
- 启用 Paint flashing(绘制闪烁)、Layout Shift Regions、FPS meter。
- Lighthouse / Web Vitals:
- 关注 LCP、CLS、FID(或 INP)、TTFB 等指标。
- Layers 面板:
- 查看合成层数量、边界。
- 结论:先量后调,避免拍脑袋优化。
9. 常见问题与误区
-
Q:display: none 和 visibility: hidden 有何差别?
- A:
display: none会让元素不参与布局,切换回来会触发回流。visibility: hidden保留占位,通常只会重绘。
- A:
-
Q:为什么改 top/left 做动画容易卡?
- A:它会改变布局或至少影响绘制,浏览器每帧需要做更多工作。用
transform: translate(...)可以走合成路径,代价更低。
- A:它会改变布局或至少影响绘制,浏览器每帧需要做更多工作。用
-
Q:读取
getBoundingClientRect()一定会回流吗?- A:只有在浏览器存在未处理的样式/布局变更时,读取会触发“强制同步布局”。如果布局是干净的,读取不一定会回流。但在高频场景中依然要避免读写交错。
-
Q:用
will-change有没有副作用?- A:会申请更多内存和资源,层多了反而拖慢合成。只在真正需要动画的元素上、动画前后短时间使用。
-
Q:使用 3D 变换
translateZ(0)就能提升性能吗?- A:它可能创建新图层,有助于走合成,但滥用会增加内存与合成成本。现代浏览器对图层管理更智能,优先用
will-change,并结合工具验证效果。
- A:它可能创建新图层,有助于走合成,但滥用会增加内存与合成成本。现代浏览器对图层管理更智能,优先用
-
Q:Grid/Flex 会不会更“费性能”?
- A:它们的布局算法复杂度可能更高,但实际性能取决于使用规模与场景。可读性与开发效率优先,遇到瓶颈再结合 DevTools 定位问题点。
-
Q:哪些属性会触发布局/绘制/合成?
- A(经验):布局类(width/height/margin/padding/top/left/font-size/line-height/display/…)→ 回流;外观类(color/background/shadow/outline)→ 重绘;合成友好(transform/opacity)→ 合成。以实际浏览器实现为准,参考 MDN 与 CSS Triggers。
10. 小结记忆卡
- 渲染管线:DOM/CSSOM → 样式计算 → 渲染树 → 布局(回流) → 绘制(重绘) → 合成。
- 代价排序(通常):回流 > 重绘 > 合成。
- 动画优先选:transform、opacity。
- 避免读写交错,集中读、集中写,利用 rAF。
- 使用 containment 与 content-visibility 优化大页面。
- 工具先量后调:Performance、Rendering、Layers、Lighthouse。
11. 进一步学习资源
- MDN Web Docs
- Rendering performance: https://developer.mozilla.org/docs/Web/Performance/Rendering
- Reflow and repaint(相关话题合集): 重排 - MDN Web 文档术语表:Web 相关术语的定义 | MDN
- content-visibility: content-visibility - CSS:层叠样式表 | MDN
- contain: contain - CSS:层叠样式表 | MDN
- web.dev(Google)
- Rendering Performance: 加载时间短 | web.dev
- Avoid large, complex layouts and layout thrashing: 避免大型、复杂的布局和布局抖动 | Articles | web.dev
- Critical Rendering Path: 关键渲染路径 | Articles | web.dev
- CSS Triggers(了解属性触发的阶段,注意可能滞后于最新实现): https://csstriggers.com/