面试回答: 浏览器解析 HTML 和 CSS,分别构建 DOM 与 CSSOM,再结合两者生成用于渲染的结构。随后经过样式计算、布局、绘制、光栅化和合成,最终把像素显示到屏幕上。页面更新时不一定重走全部流程:尺寸或位置变化需要重新布局,颜色和阴影等视觉变化通常从绘制开始;独立合成层上的
transform、opacity动画则可能只需要重新合成。性能问题通常来自长任务、频繁布局、复杂绘制和图层过多,其中“写样式后立即读取几何信息”会触发强制同步布局,循环发生时就会形成布局抖动。
浏览器把页面显示出来,可以用下面这条主线理解:
浏览器解析 HTML,将标签、属性和文本转换为 DOM 节点并构建 DOM 树。HTML 解析通常是渐进的,并不是等整个文件下载完成后才开始。
普通同步脚本可能暂停 HTML 解析,因为脚本可能通过 document.write 或 DOM API 改变当前文档结构。defer、async 和模块脚本的加载与执行时机不同,详见 defer 和 async 的区别。
浏览器解析样式表,形成 CSSOM,并计算每个元素最终命中的样式规则。
CSS 通常不会阻止 HTML 继续解析,但会阻塞页面渲染;某些同步脚本还需要等待前面的样式表加载完成,因为脚本可能读取元素的计算样式。
浏览器结合 DOM 和计算后的样式,确定哪些元素需要生成布局盒以及它们之间的关系。
display: none 的元素不生成布局盒,也不占据空间;visibility: hidden 的元素仍然参与布局,只是不绘制可见内容;因此,“渲染树就是 DOM 和 CSSOM 简单合并”只是便于入门的简化说法。
布局阶段根据视口、盒模型和布局规则,计算元素的尺寸与位置。历史资料常把重新执行布局称为 Reflow 或回流。
布局可能只影响局部子树,也可能沿父子关系扩散。影响范围取决于布局模型、元素依赖关系和浏览器实现,并非每次都重新计算整张页面。
绘制阶段把背景、文字、边框、阴影等视觉内容转换为绘制指令。光栅化再把这些指令转换为可以显示的像素块。
复杂阴影、滤镜、大面积渐变和频繁变化的绘制区域,都可能增加这一阶段的成本。
页面可能被拆分为多个合成层。合成线程将已经光栅化的图块按正确顺序组合,并提交为最终画面。
如果元素已经位于合适的独立合成层,修改 transform 或 opacity 往往不需要重新布局和绘制,只需要更新图层的变换或透明度。但是否能够只走合成阶段取决于分层状态和浏览器实现,不能把它理解为无条件规则。
不同属性变化影响的渲染阶段不同:
| 变化类型 | 常见例子 | 通常需要执行的阶段 |
|---|---|---|
| 结构或几何变化 | 增删可见节点、修改宽高、边距、字体大小 | Layout → Paint → Composite |
| 纯视觉变化 | color、background、box-shadow | Paint → Composite |
| 合成属性变化 | 独立层上的 transform、opacity | Composite |
这张表表达的是常见路径,不是所有浏览器、所有属性组合下的绝对保证。例如修改文字颜色不需要重新布局,但仍可能扩大需要重绘的区域;修改 transform 时,如果元素没有合适的分层,也可能产生额外绘制工作。
当元素的结构、尺寸或位置发生变化时,浏览器需要重新计算受影响元素的几何信息。
常见触发来源包括:
width、height、margin、padding、border 等尺寸属性;display 等改变布局参与状态的属性。元素几何信息不变,但视觉外观发生变化时,浏览器通常只需要重新生成或更新绘制内容。
常见例子包括:
color、background-color;box-shadow、border-radius;visibility;常见面试结论是:回流通常会带来后续的绘制与合成,重绘则不一定需要重新布局,布局成本通常高于单纯绘制。
这个结论比“回流一定重绘整个页面”更准确,因为现代浏览器会尽量限制无效区域与绘制范围。
浏览器会尽量把连续的样式修改批量处理,等 JavaScript 任务结束或进入合适的渲染时机后再统一更新布局。
但是,如果代码刚修改了布局相关样式,紧接着又读取依赖最新布局结果的几何信息,浏览器为了返回正确数值,只能先同步完成样式计算和布局:
可能触发布局刷新或样式计算的读取包括:
offsetWidth、offsetHeight、offsetTop、offsetLeft;clientWidth、clientHeight;scrollWidth、scrollHeight、scrollTop;getBoundingClientRect();getComputedStyle()。读取本身不一定每次都造成布局。如果此前没有让样式或布局失效,浏览器可以直接返回已有结果。真正危险的是写入与读取交替发生。
在循环中反复“写 → 读”,可能让浏览器每轮都被迫同步布局:
改为先集中读取,再集中写入:
读写分离减少的是强制同步布局次数,并不意味着整个批量更新永远只产生一次局部计算。浏览器最终仍然需要为真实的几何变化执行布局。
更多触发属性和调度示例见布局抖动与强制同步布局。
transform 和opacity 更适合动画?使用 top、left 或宽高制作动画通常会改变几何信息,需要反复布局。transform 和 opacity 不改变普通文档流中的布局结果,在元素被合适地分层后,可以交给合成阶段处理。
这不代表所有 transform 动画都没有成本:图层首次创建、纹理上传、超大图层光栅化和图层之间的叠加,都可能影响性能。
will-change 为什么不能滥用?will-change 是对浏览器的优化提示,可能让浏览器提前为变化做准备:
独立图层需要显存和管理成本。给大量列表项长期设置 will-change,可能造成内存增长和合成压力。更合理的做法是在元素即将发生高频变化前使用,并在变化结束后移除。
先测量,再决定优化位置。
录制卡顿操作,重点观察:
Recalculate Style 和 Layout 是否频繁出现;Paint;Chrome DevTools 的 Rendering 面板可以辅助观察绘制闪烁、帧率和图层边界。发现问题后,再回到调用栈和具体 DOM 更新定位原因。
transform、opacity。requestAnimationFrame 对齐下一次绘制,但不要在同一个回调中继续反复读写布局。will-change。requestAnimationFrame 只能帮助代码与渲染时机对齐,本身不会自动消除长任务或布局抖动。
CSSOM 是确定页面样式和布局的必要输入。尽早加载关键 CSS,可以避免页面先以无样式状态出现,再发生明显跳变。
同步脚本会暂停 HTML 解析。现代项目通常使用 defer、模块脚本、代码分割或合理的资源优先级,而不是简单地把所有脚本机械移动到 body 底部。
display: none、visibility: hidden 和opacity: 0 有什么区别?| 属性 | 是否占据布局空间 | 常见渲染影响 |
|---|---|---|
display: none | 否 | 切换时通常需要重新布局 |
visibility: hidden | 是 | 通常不需要重新布局,但需要更新绘制结果 |
opacity: 0 | 是 | 适合透明度动画;元素默认仍可能接收指针事件 |
offsetWidth 一定会触发回流吗?不一定。如果布局结果仍然有效,可以直接读取缓存结果;如果前面有尚未处理的布局变更,浏览器才需要先同步完成布局。
布局需要计算元素间的几何依赖,影响可能沿父子或兄弟关系扩散;绘制主要更新视觉指令和像素。布局完成后通常还要继续绘制和合成,因此整体工作更多。
不要直接归因于回流。应依次检查 JavaScript 长任务、样式计算、布局、绘制、图层与资源加载,用 Performance 记录确认真正的瓶颈。