前言
当我们在浏览器中打开一个网页时,往往只需要短短几秒,就能看到五彩斑斓的界面。你有没有想过,这一过程背后究竟发生了什么?浏览器是如何一步步把一段 HTML、CSS 和 JavaScript 代码变成我们可以看到和交互的页面的?
要理解网页性能优化,了解渲染过程和关键机制,例如 回流(reflow)、重绘(repaint) 等,是每个前端开发者必须掌握的必修课。
这其中,回流与重绘作为性能"潜规则",在页面渲染性能中扮演着至关重要的角色。它们看似安静地藏在幕后,却往往在不经意间拖慢我们的页面表现。 尤其是在频繁 DOM 操作、动画和复杂的响应交互中,回流的"昂贵成本"常常会让应用出现卡顿、不流畅的问题。
本文将从浏览器最基础的渲染过程出发,带你走进网页绘制的世界,逐步揭示DOM 树、CSSOM、渲染树、布局、绘制、复合图层等核心概念。
渲染过程
在了解回流和重绘之前,我们要首先了解渲染过程 ,即浏览器是如何把 HTML、CSS 和 JavaScript 代码变成我们可以看到和交互的页面的。
浏览器的渲染大概是这样的:

1.构建DOM树
2.构建CSS树
3.构建Render树
4.节点布局
5.页面渲染
浏览器有两种引擎,一种是渲染引擎,一种是JS引擎,在渲染的过程中,是渲染引擎在进行。
渲染引擎首先通过网络获得所请求文档的内容, 下面是渲染引擎在取得内容之后的基本流程:
- 引擎开始解析
html
和css
,按照编码格式对拿到的字节码进行解码:对html
开始解析,识别开关标签,识别各种属性...最后形成DOM树
,对css
进行解码,拿到css
文本,进行词法分析,最后构建CSSOM树
- 合并
DOM树
和CSSOM树
,合并为Render Tree
,Render Tree
只包含需要展示的内容 - 开始布局 (Layout/Reflow),也叫
回流
,浏览器计算每个节点在页面中的具体位置和尺寸,确定每个元素在页面的确切位置 - 绘制 (Paint),浏览器将每一个可视元素转化为屏幕上的实际像素 ,这个过程中会绘制文字、图像、颜色、边框、阴影等,引擎会一个个像素画上去。
- 合成:
- 页面可能会分成多个图层(Layer);
- 每个图层可能单独绘制,最终由 合成线程 合并显示;
- 使用
transform
、opacity
等属性时,浏览器会使用 GPU 加速渲染; - 合图层(Compositing)是现代浏览器提升动画性能的重要机制。
什么是DOM树/CSSOM树/Render树?
DOM(Document Object Model) 是浏览器解析 HTML 后生成的一个树状结构(对象模型),表示页面的结构。它是一个编程接口,通过它可以访问和操作 HTML 文档的内容、结构和样式。
CSSOM(CSS Object Model) 是浏览器解析 CSS 样式后生成的树状结构,表示页面中样式规则。它决定了每个 DOM 元素的样式(如颜色、布局等)如何应用。
Render Tree 是 DOM和CSSOM的结合,其只会包含能够显示在页面的内容。
假设有以下代码:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<h1>回流和重绘</h1>
<p>回流是啥呢?</p>
</body>
</html>
则DOM树结构为:

CSS树结构类似于下面这样(在网上随便找了个图):

Render树是它们两个的结合,将对应的html元素节点与css样式结合起来了:

渲染过程中遇到JS?
相信大家一直以来都有一个习惯,那就是把<script>
脚本放在<body>
的最下方,那么你有没有想过为什么呢?
实际上,这种行为有利地避免了页面卡死 的问题,当我们打开一个网页的时候,网页向服务器发送请求,拿到html
、css
来进行绘制,它是从上到下解析,当JS
放在上方时,渲染引擎遇见JS就会停止当下工作,转而将管理权交给JS引擎 ,JS引擎开始执行JS代码,一直到JS代码执行完毕以后才会再将执行权交给渲染引擎 。由此就会造成页面阻塞,DOM
元素没有加载出来,样式也加载不出来。
More,如果JS代码中有操作DOM元素的操作或者修改CSS的操作时会怎么样呢?
操作尚未加载的DOM?
由于dom
元素没有被解析,这个时候获得的结果会返回null
操作没有形成树的CSS?
但是操作css时就不一样了,引擎会先加载css
,使得没加载完毕的css
加载完毕并构建完整的CSSOM树
,再返回来执行JS
,也就是说,在这种情况下,浏览器会先下载和构建CSSOM,然后再执行JavaScript,最后在继续构建DOM。
这样造成了页面的阻塞,页面就加载不出来,加载就慢,也违背了我们前端以用户体验为中心的宗旨。
回流/重绘
介绍完了渲染过程 ,现在我们知道了回流/重绘就是渲染的两个过程。接下来我们来详细介绍一下这两位大神!
回流
回流 (Reflow 有时也叫 Layout )是浏览器根据 DOM 和 CSSOM 构建 Render Tree 后,计算每个可视节点在页面上的几何属性(如位置、大小)的过程。
- 代价昂贵 :回流需要计算位置和几何信息,会引发大量计算,非常耗费性能。
- 例子:
<table>
中任意元素发生改变时,就要对整个table进行回流和重绘,因为一个地方变了 ,整个table的结构就都改变 了,所以要回流重绘一整个table,如果table包含的元素足够复杂,那么会消耗一大笔性能,到时候就等着被骂了55555~
- 例子:
什么时候会触发回流?
回流用于计算能够看到的节点在页面上的位置大小 ,也就是说,当节点的位置或者大小发生改变 的时候,就会触发回流呗~
这样的话,页面首次加载时一定会触发回流:因为第一次加载,将所有元素的位置都要进行排列
其次就是改变了元素位置/大小的操作时会触发回流。
下面是一些会触发回流的操作:
操作 | 触发回流? |
---|---|
添加或删除可见的 DOM 元素 | ✅ |
元素尺寸发生变化(width/height) | ✅ |
内容变化(如文本、图片加载) | ✅ |
浏览器窗口大小变化(resize) | ✅ |
样式属性读取(例如 offsetTop , clientWidth 等) |
✅(需配合写操作时才明显) |
设置 style 属性 |
✅ 如果影响布局 |
激活伪类(如 :hover ) |
✅ |
获取布局信息(如 getBoundingClientRect() ) |
✅ |
例如下面的操作:
js
const element = document.querySelector('#box'); // 假设有一个box元素
const width = element.offsetWidth; // 这里可能触发回流
element.style.width = '200px'; // 再次触发回流
重绘
重绘是浏览器在不需要重新布局的情况下,将可视部分重新绘制在屏幕上的过程。
- 相对轻微:重绘只影响元素的"外观",不影响元素的大小和位置。
什么时候会触发重绘
当元素属性改变,但是改变不影响布局的时候,会触发重绘。
下面是一些触发重绘的操作:
样式属性 | 触发重绘? |
---|---|
color |
✅ |
background-color |
✅ |
visibility |
✅ |
outline |
✅ |
box-shadow 修改颜色(不影响尺寸) |
✅ |
回流与重绘的关系
- 回流必定引发重绘:元素布局改变了,当然要重新绘制。
- 重绘不一定引发回流:比如颜色变了,布局没有改,所以只重绘。
如何理解呢?我们要明白页面展现的内容都是由渲染引擎一个像素点一个像素点画出来的 ,当布局改变 的时候,引擎就要擦掉重新画,一定会触发回流与重绘 ,当颜色等不会影响布局的属性改变 的时候,引擎只需要在原地修改一下就行了,不需要修改布局,所以只触发重绘。
display:none
的元素是否会触发回流与重绘?
这是一个经典的问题,当我有一个元素,但是元素不显示的时候,它会不会参与回流重绘呢?
这需要看看回流和重绘的前提了:回流和重绘都是基于render tree
来的,render tree
中只会包含显示在页面的内容。
由此一来,display:none
的元素压根就不参与render tree
的构建,回流与重绘的对象都必须在render tree
中,就像是
领导说:"你给我照着示例图画一个苹果"
结果你接过来示例图,发现是空的,压根画不了。
但是,虽然display:none
的元素不参与render tree
的构建,但它会参与dom
和cssom
树的构建。
举个例子:
js
<div id="hidden" style="display: none">隐藏元素</div>
<div id="visible">可见元素</div>
#hidden
不会出现在渲染树中- 所以:
- 修改
#hidden
的宽高、内容等,不会触发回流 - 修改
#hidden
的颜色、背景等,不会触发重绘
- 修改
⚠️display:none
&visibility: hidden
的区别
属性 | 是否占空间 | 是否参与 Layout | 是否参与 Paint | 是否占渲染树 |
---|---|---|---|---|
display: none |
❌ 不占空间 | ❌ 不参与 | ❌ 不参与 | ❌ 不在渲染树 |
visibility: hidden |
✅ 占空间 | ✅ 参与 | ✅ 参与(绘制透明) | ✅ 在渲染树 |
visibility: hidden
仍然是参与render tree
的构建的,只不过透明度是满的,但它是存在的,所以具有这个属性的元素也会触发回流与重绘。
结语
浏览器的渲染机制远比我们想象的要复杂,从 HTML 解析到最终像素呈现在屏幕上,每一步都涉及多个关键阶段:DOM 构建、CSSOM 构建、渲染树生成、回流、重绘与图层合成。而在这其中,JavaScript 的执行、样式操作以及元素的显示控制方式,都会对性能产生深远影响。
通过理解 display: none
与 visibility: hidden
的区别,我们可以更合理地选择隐藏策略;通过了解脚本执行与 CSSOM、DOM 的依赖关系,我们可以避免在错误的时机访问未就绪的元素;通过认识回流与重绘的成本,我们可以写出更高效的 DOM 操作代码,避免不必要的性能损耗。
性能优化不是一蹴而就的事情,它是一门细致的艺术。掌握浏览器的渲染原理,是每一个前端开发者走向进阶的必经之路。希望本文能为你构建起一个清晰的认知框架,帮助你在今后的开发中,写出更优秀、更高效的代码。