Skip to content

浏览器渲染流水线

理解浏览器如何将 HTML/CSS/JS 变成像素,是做性能优化的基础。

渲染流水线全景

HTML → DOM Tree ─┐
                  ├→ Render Tree → Layout → Paint → Composite → 屏幕
CSS  → CSSOM    ─┘

详细步骤:
1. Parse HTML      → DOM Tree
2. Parse CSS       → CSSOM Tree
3. Combine         → Render Tree(只含可见节点)
4. Layout          → 计算每个节点的几何信息(位置、大小)
5. Paint           → 生成绘制指令(不是真正绘制)
6. Composite       → 合成层,GPU 渲染到屏幕

关键渲染路径

HTML 解析(Parser)
  ├── 遇到 <script>(无 async/defer)→ 阻塞解析,下载并执行 JS
  ├── 遇到 <link rel="stylesheet"> → 不阻塞解析,但阻塞渲染
  └── 遇到 <img> → 异步下载,不阻塞

优化关键渲染路径:
  <link rel="stylesheet"> 放 <head>(尽早加载 CSS)
  <script> 放 </body> 前,或使用 defer/async
  减少关键资源数量和大小

script 加载策略

html
<!-- 阻塞解析:下载 + 执行完才继续解析 HTML -->
<script src="app.js"></script>

<!-- async:下载不阻塞,下载完立即执行(执行时阻塞解析)-->
<!-- 适合:独立脚本,如统计代码 -->
<script async src="analytics.js"></script>

<!-- defer:下载不阻塞,HTML 解析完后按顺序执行 -->
<!-- 适合:依赖 DOM 的脚本,保证执行顺序 -->
<script defer src="app.js"></script>

<!-- type="module":默认 defer 行为 -->
<script type="module" src="app.js"></script>

重排(Reflow)与重绘(Repaint)

重排(Layout):几何属性变化,需要重新计算布局
  触发:width/height/margin/padding/position/display/font-size...
  代价:最高(影响整个渲染树)

重绘(Paint):外观变化,不影响布局
  触发:color/background/border-color/visibility/box-shadow...
  代价:中等

合成(Composite):只在合成层操作
  触发:transform/opacity(在合成层上)
  代价:最低(GPU 处理,不影响主线程)

强制同步布局(Layout Thrashing)

js
// ❌ 读写交替:每次读取都强制触发重排
for (const el of elements) {
  const width = el.offsetWidth  // 读:强制重排
  el.style.width = width * 2 + 'px'  // 写:标记脏
}
// 每次循环都触发重排,性能极差

// ✅ 批量读,批量写
const widths = elements.map(el => el.offsetWidth)  // 批量读
elements.forEach((el, i) => {
  el.style.width = widths[i] * 2 + 'px'  // 批量写
})
// 只触发一次重排

// ✅ 使用 requestAnimationFrame 批处理
function updateLayout() {
  requestAnimationFrame(() => {
    // 在下一帧统一处理 DOM 操作
    elements.forEach(el => { /* ... */ })
  })
}

合成层(Composite Layer)

css
/* 触发合成层的属性 */
.will-animate {
  /* 提升为合成层,transform/opacity 动画不触发重排重绘 */
  will-change: transform;
  /* 或 */
  transform: translateZ(0);  /* 老方法,hack */
}

/* ✅ 高性能动画:只用 transform 和 opacity */
.slide-in {
  animation: slideIn 0.3s ease;
}
@keyframes slideIn {
  from { transform: translateX(-100%); opacity: 0; }
  to   { transform: translateX(0);     opacity: 1; }
}

/* ❌ 低性能动画:触发重排 */
.bad-animation {
  animation: badSlide 0.3s ease;
}
@keyframes badSlide {
  from { left: -100px; }  /* 触发重排! */
  to   { left: 0; }
}

虚拟滚动

jsx
// 长列表优化:只渲染可视区域内的元素
function VirtualList({ items, itemHeight = 50, containerHeight = 500 }) {
  const [scrollTop, setScrollTop] = useState(0)

  const startIndex = Math.floor(scrollTop / itemHeight)
  const endIndex = Math.min(
    startIndex + Math.ceil(containerHeight / itemHeight) + 1,
    items.length
  )

  const visibleItems = items.slice(startIndex, endIndex)
  const totalHeight = items.length * itemHeight
  const offsetY = startIndex * itemHeight

  return (
    <div
      style={{ height: containerHeight, overflow: 'auto' }}
      onScroll={e => setScrollTop(e.target.scrollTop)}
    >
      {/* 撑开滚动高度 */}
      <div style={{ height: totalHeight, position: 'relative' }}>
        {/* 只渲染可见项 */}
        <div style={{ transform: `translateY(${offsetY}px)` }}>
          {visibleItems.map((item, i) => (
            <div key={startIndex + i} style={{ height: itemHeight }}>
              {item.content}
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}

Web Vitals 核心指标

LCP(Largest Contentful Paint)— 最大内容绘制
  目标:< 2.5s
  优化:预加载关键资源、优化图片、减少服务器响应时间

FID(First Input Delay)→ INP(Interaction to Next Paint,2024 替代)
  目标:INP < 200ms
  优化:减少长任务、代码分割、Web Worker

CLS(Cumulative Layout Shift)— 累积布局偏移
  目标:< 0.1
  优化:为图片/视频指定尺寸、避免动态插入内容

FCP(First Contentful Paint)— 首次内容绘制
  目标:< 1.8s
  优化:减少渲染阻塞资源、内联关键 CSS

TTFB(Time to First Byte)— 首字节时间
  目标:< 800ms
  优化:CDN、服务器优化、缓存策略

总结

  • 渲染流水线:Parse → Style → Layout → Paint → Composite
  • 重排代价最高,重绘次之,合成层操作最快
  • 动画只用 transformopacity,触发合成而非重排
  • 避免强制同步布局:批量读取,批量写入
  • 关注 Core Web Vitals:LCP、INP、CLS

系统学习 Web 前端生态,深入底层架构