浏览器渲染原理

从输入 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 避开回流。

posted on 2026-08-06 15:17  zy89898976  阅读(15)  评论(0)    收藏  举报