Skip to content

浏览器渲染原理与性能优化实战 ​

理解浏览器如何将 HTML/CSS/JS 变成像素,是前端性能优化的基石。本文从渲染流水线出发,深入分析重排、重绘、合成层,并给出可落地的优化方案。

一、浏览器渲染流水线 ​

浏览器将一个页面渲染到屏幕,需要经历以下关键步骤:

HTML → DOM Tree
CSS  → CSSOM Tree
         ↓
     Render Tree(合并)
         ↓
     Layout(布局/重排)
         ↓
     Paint(绘制/重绘)
         ↓
     Composite(合成)
         ↓
     屏幕像素

1.1 构建 DOM 与 CSSOM ​

浏览器解析 HTML 字节流,经过 字节 → 字符 → Token → 节点 → DOM 的过程构建 DOM 树。与此同时,CSS 文件被解析为 CSSOM(CSS Object Model)。

关键点:

  • CSS 是渲染阻塞资源:CSSOM 未构建完成,浏览器不会进行渲染
  • JS 是解析阻塞资源:遇到 <script> 标签,HTML 解析暂停(除非加了 async/defer)
html
<!-- 阻塞渲染 -->
<link rel="stylesheet" href="style.css" />

<!-- 阻塞 HTML 解析 -->
<script src="app.js"></script>

<!-- 不阻塞解析,DOMContentLoaded 后执行 -->
<script defer src="app.js"></script>

<!-- 不阻塞解析,下载完立即执行 -->
<script async src="analytics.js"></script>

1.2 Render Tree ​

DOM + CSSOM 合并生成 Render Tree,只包含可见节点(display: none 的节点不在其中,visibility: hidden 的节点在其中)。

1.3 Layout(布局) ​

计算每个节点的几何信息:位置、尺寸。这一步也叫 Reflow(重排)。

1.4 Paint(绘制) ​

将节点转换为屏幕上的实际像素,填充颜色、文字、阴影等。这一步也叫 Repaint(重绘)。

1.5 Composite(合成) ​

现代浏览器会将页面分成多个合成层(Compositing Layer),由 GPU 独立绘制后合并输出。这是性能优化的关键所在。


二、重排(Reflow)与重绘(Repaint) ​

2.1 触发重排的操作 ​

重排代价极高,会触发整个渲染流水线重新执行。

操作类型示例
修改几何属性width、height、margin、padding、border
修改布局属性display、position、float
读取布局信息offsetWidth、getBoundingClientRect()
窗口变化resize 事件
字体变化font-size、font-family
javascript
// 错误 强制同步布局(Layout Thrashing)
// 每次读取都会强制浏览器刷新布局
for (let i = 0; i < elements.length; i++) {
  const width = elements[i].offsetWidth  // 读取 → 触发重排
  elements[i].style.width = width + 10 + 'px'  // 写入
}

// 正确 批量读取,批量写入
const widths = elements.map(el => el.offsetWidth)  // 批量读取
elements.forEach((el, i) => {
  el.style.width = widths[i] + 10 + 'px'  // 批量写入
})

2.2 触发重绘的操作 ​

只修改外观,不影响布局,代价比重排小,但仍需避免频繁触发。

color、background-color、visibility、box-shadow、border-radius...

2.3 只触发合成的操作(最优) ​

以下属性的变化只触发合成层重新合并,不触发重排和重绘,性能最佳:

  • transform
  • opacity
  • filter(部分情况)
  • will-change
css
/* 错误 触发重排 */
.box {
  left: 100px;
  top: 100px;
}

/* 正确 只触发合成,GPU 加速 */
.box {
  transform: translate(100px, 100px);
}

三、合成层(Compositing Layer)原理 ​

3.1 什么是合成层 ​

浏览器将某些元素提升为独立的合成层,由 GPU 单独处理。合成层的变化不会影响其他层,因此动画极其流畅。

触发合成层提升的条件:

css
/* 1. 使用 transform 3D 变换 */
transform: translateZ(0);
transform: translate3d(0, 0, 0);

/* 2. 使用 will-change 声明 */
will-change: transform, opacity;

/* 3. 使用 opacity 动画(配合 will-change) */
will-change: opacity;

/* 4. video、canvas、iframe 元素 */
/* 5. position: fixed */
/* 6. CSS filter */
filter: blur(4px);

3.2 合成层的代价 ​

合成层并非越多越好!每个合成层都需要额外的内存和管理开销。

javascript
// 检查页面合成层(Chrome DevTools)
// 打开 DevTools → Layers 面板,可以看到所有合成层

合成层爆炸(Layer Explosion):

css
/* 错误 危险:will-change 滥用 */
* {
  will-change: transform; /* 所有元素都变成合成层,内存爆炸 */
}

/* 正确 正确:只对需要动画的元素使用 */
.animated-card {
  will-change: transform;
}

/* 正确 动画结束后移除 */
.animated-card.done {
  will-change: auto;
}

四、实战优化技巧 ​

4.1 使用 requestAnimationFrame ​

将 DOM 操作放入 rAF,确保在浏览器下一帧渲染前执行,避免丢帧。

javascript
// 错误 可能在一帧内触发多次重排
function animate() {
  element.style.left = getNewPosition() + 'px'
  setTimeout(animate, 16)
}

// 正确 与浏览器渲染节奏同步
function animate() {
  element.style.transform = `translateX(${getNewPosition()}px)`
  requestAnimationFrame(animate)
}

4.2 虚拟 DOM 批量更新 ​

React/Vue 的虚拟 DOM 本质上就是在做批量 DOM 操作,减少重排次数。

javascript
// 手动实现批量更新
function batchUpdate(updates) {
  // 使用 DocumentFragment 离线操作
  const fragment = document.createDocumentFragment()

  updates.forEach(({ tag, text }) => {
    const el = document.createElement(tag)
    el.textContent = text
    fragment.appendChild(el)
  })

  // 一次性插入,只触发一次重排
  document.getElementById('container').appendChild(fragment)
}

4.3 CSS 动画 vs JS 动画 ​

css
/* 正确 CSS 动画:浏览器可以在合成线程执行,不阻塞主线程 */
.slide-in {
  animation: slideIn 0.3s ease-out;
}

@keyframes slideIn {
  from { transform: translateX(-100%); }
  to   { transform: translateX(0); }
}
javascript
// 正确 Web Animations API:兼具 CSS 动画的性能和 JS 的灵活性
element.animate(
  [
    { transform: 'translateX(-100%)' },
    { transform: 'translateX(0)' }
  ],
  {
    duration: 300,
    easing: 'ease-out',
    fill: 'forwards'
  }
)

4.4 避免强制同步布局的工具函数 ​

javascript
/**
 * 安全读取布局信息,避免 Layout Thrashing
 * 将所有读操作收集到微任务前执行
 */
class LayoutReader {
  constructor() {
    this._reads = []
    this._writes = []
    this._scheduled = false
  }

  read(fn) {
    this._reads.push(fn)
    this._schedule()
  }

  write(fn) {
    this._writes.push(fn)
    this._schedule()
  }

  _schedule() {
    if (this._scheduled) return
    this._scheduled = true
    requestAnimationFrame(() => {
      // 先批量读取
      this._reads.forEach(fn => fn())
      this._reads = []
      // 再批量写入
      this._writes.forEach(fn => fn())
      this._writes = []
      this._scheduled = false
    })
  }
}

const layout = new LayoutReader()

// 使用
layout.read(() => {
  const width = element.offsetWidth
  layout.write(() => {
    element.style.width = width * 2 + 'px'
  })
})

4.5 Intersection Observer 替代滚动监听 ​

javascript
// 错误 scroll 事件频繁触发,每次都读取 getBoundingClientRect
window.addEventListener('scroll', () => {
  elements.forEach(el => {
    const rect = el.getBoundingClientRect()  // 触发重排!
    if (rect.top < window.innerHeight) {
      el.classList.add('visible')
    }
  })
})

// 正确 Intersection Observer:浏览器原生支持,无需手动计算
const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        entry.target.classList.add('visible')
        observer.unobserve(entry.target)  // 只触发一次
      }
    })
  },
  { threshold: 0.1 }
)

elements.forEach(el => observer.observe(el))

五、性能分析工具 ​

5.1 Chrome DevTools Performance 面板 ​

  1. 打开 DevTools → Performance
  2. 点击录制,操作页面,停止录制
  3. 查看 Main 线程的火焰图
  4. 紫色 = Layout(重排),绿色 = Paint(重绘),黄色 = Scripting

关键指标:

  • FPS:帧率,60fps = 每帧 16.7ms,低于 30fps 用户明显感知卡顿
  • Long Tasks:超过 50ms 的任务,会阻塞主线程
  • Layout Shift:布局偏移,影响 CLS(Cumulative Layout Shift)

5.2 用代码测量渲染性能 ​

javascript
// 使用 Performance API 测量
performance.mark('render-start')

// ... 执行 DOM 操作 ...

performance.mark('render-end')
performance.measure('render-duration', 'render-start', 'render-end')

const [measure] = performance.getEntriesByName('render-duration')
console.log(`渲染耗时: ${measure.duration.toFixed(2)}ms`)

六、核心优化清单 ​

优先级优化项说明
高避免 Layout Thrashing批量读写,不要交替读写布局属性
高动画使用 transform/opacity只触发合成,不触发重排重绘
中合理使用 will-change提前提升合成层,但不要滥用
中使用 rAF 调度动画与浏览器渲染节奏同步
中用 Intersection Observer替代 scroll 事件中的位置计算
低CSS 动画优于 JS 动画可在合成线程执行,不阻塞主线程
低DocumentFragment 批量插入减少 DOM 操作次数

总结 ​

浏览器渲染性能优化的核心思路只有一条:减少主线程的工作量,让 GPU 多干活。

  • 能用 transform 就不用 left/top
  • 能批量操作就不要逐个操作
  • 能用 CSS 动画就不用 JS 动画
  • 能用 Observer API 就不用 scroll 事件

理解了渲染流水线,你就能在写代码时自然地做出正确选择,而不是事后靠猜测去优化。


本文由小虾子 撰写

Last updated: