浏览器渲染原理
从输入 URL 到页面显示,浏览器干了这些事:
一、整体流程
HTML 文本
↓
解析 HTML → DOM 树
↓
解析 CSS → CSSOM 树
↓
DOM + CSSOM → Render 树(渲染树)
↓
Layout(布局/回流)→ 计算每个元素的位置和大小
↓
Paint(绘制/重绘)→ 像素画到屏幕上
↓
Composite(合成)→ 分层合成,最终显示
二、逐步拆解:
1. 构建 DOM 树
<html>
<head>
<link rel="stylesheet" href="style.css"> ← 遇到 CSS,下载+解析
<script src="app.js"></script> ← 遇到 JS,下载+执行(阻塞!)
</head>
<body>
<div>
<p>hello</p>
</div>
</body>
</html>
浏览器从上到下读 HTML,一个个节点构建 DOM 树:
Document
└── html
├── head
│ ├── link
│ └── script
└── body
└── div
└── p
└── "hello"
2. 构建 CSSOM 树
CSS 下载完,解析成 CSSOM(CSS Object Model):
css
div { color: red; }
p { font-size: 14px; }
CSSOM
└── div → { color: red }
└── p → { font-size: 14px }
3. 构建渲染树(Render Tree)
DOM + CSSOM 合并,只保留可见元素:
渲染树
└── div (color: red)
└── p (font-size: 14px, color: red)
└── "hello"
注意:display: none 的元素不会出现在渲染树里! visibility: hidden 的元素会出现(占位但看不见)。
| DOM树 | 渲染树 | |
| display: none | ✅ 有 | ❌ 没有 |
| visibility: hidden | ✅ 有 | ✅ 有(占位) |
| opacity: 0 | ✅ 有 | ✅ 有(占位) |
4. Layout(布局/回流/Reflow)
计算每个元素的位置和大小:
这个 div 宽多少?高多少?
这个 p 在第几行第几个?
margin/padding 多少?
这一步很耗性能,因为要递归计算整棵树。
5. Paint(绘制/重绘/Repaint)
把布局结果画成像素:
文字怎么渲染
背景色怎么填
边框怎么画
图片怎么贴
6. Composite(合成)
现代浏览器会把页面分成多个图层(layer),各自绘制,最后合成:
图层1:背景
图层2:普通内容
图层3:transform 动画元素
图层4:fixed 定位元素
↓
GPU 合成 → 屏幕显示
三、JS 对渲染的影响
<div id="app"></div> <script src="app.js"></script> <!-- JS 会阻塞 HTML 解析 -->
遇到 <script> 时,浏览器会停下来:
暂停 HTML 解析
下载 JS(如果没有 async/defer)
执行 JS 继续解析 HTML
因为 JS 可能会修改 DOM,所以必须等它执行完。
解决阻塞的方法
<!-- 普通脚本:阻塞 --> <script src="app.js"></script> <!-- async:下载完就执行,不保证顺序 --> <script async src="a.js"></script> <!-- defer:等 HTML 解析完再执行,按顺序 --> <script defer src="a.js"></script>
| 阻塞 HTML | 执行时机 | 顺序 | |
| 普通 script | ✅ 阻塞 | 立刻 | 按书写顺序 |
| async | ❌ 不阻塞 | 下载完立刻 | 不保证 |
| defer | ❌ 不阻塞 | DOMContentLoaded 前 |
按书写顺序 |
四、回流和重绘
回流(Reflow)= 重新 Layout
元素的位置或大小变了,要重新计算布局:
// 这些操作会触发回流 element.style.width = '100px' element.style.height = '200px' element.style.margin = '10px' element.style.display = 'none' element.offsetLeft // 读取布局信息也会触发!
重绘(Repaint)= 重新 Paint
外观变了但位置大小没变,只重新绘制:
// 这些操作只触发重绘(不回流) element.style.color = 'red' element.style.backgroundColor = 'blue' element.style.visibility = 'hidden'
性能关系
回流 → 一定会重绘
重绘 → 不一定会回流
回流代价 >> 重绘代价
五、如何减少回流
❌ 坏写法:连续改样式,每次都可能回流
element.style.width = '100px' // 回流1 element.style.height = '200px' // 回流2 element.style.margin = '10px' // 回流3
✅ 好写法1:用 class 一次性改
element.className = 'new-style' // 只回流1次
✅ 好写法2:先脱离文档流,改完再放回来
element.style.display = 'none' // 隐藏(不在渲染树里了) element.style.width = '100px' element.style.height = '200px' element.style.margin = '10px' element.style.display = 'block' // 放回来,只回流1次
✅ 好写法3:用 DocumentFragment 批量操作 DOM
const fragment = document.createDocumentFragment() for (let i = 0; i < 100; i++) { const li = document.createElement('li') li.textContent = i fragment.appendChild(li) // 不触发回流 } ul.appendChild(fragment) // 只回流1次
✅ 好写法4:避免频繁读取布局属性
// ❌ 读写交替,每次读都强制回流 for (let i = 0; i < 100; i++) { element.style.width = element.offsetWidth + 10 + 'px' // ↑ 读布局(回流) ↑ 写样式(回流) } // ✅ 先读完,再写 const width = element.offsetWidth // 读1次 element.style.width = width + 1000 + 'px' // 写1次
六、CSS 优化:利用合成层
有些 CSS 属性只触发合成,不触发回流和重绘,性能最好:
css
/* ✅ 只触发合成(GPU 加速) */ transform: translateX(100px) transform: rotate(45deg) transform: scale(1.5) opacity: 0.5 /* ❌ 触发回流 */ left: 100px top: 50px width: 200px
做动画优先用 transform 和 opacity,别用 left/top/width。
七、完整流程图
HTML 下载
↓
从上到下解析 HTML → DOM 树
├─ 遇到 CSS → 下载+解析 → CSSOM 树
├─ 遇到 JS → 下载+执行(阻塞 HTML 解析)
└─ 遇到 img → 异步下载(不阻塞)
↓
DOM + CSSOM → 渲染树(过滤不可见元素)
↓
Layout → 计算位置大小(回流)
↓
Paint → 画像素(重绘)
↓
Composite → 分层合成(GPU)
↓
屏幕显示 🎉
一句话:浏览器渲染就是"建 DOM 树 → 建 CSSOM → 合成渲染树 → 算布局(回流)→ 画像素(重绘)→ 合成显示",JS 阻塞解析,回流最耗性能,动画用 transform 避开回流。
浙公网安备 33010602011771号