浏览器渲染流水线完全指南:从 DOM 解析到 Composite

"为什么我的页面那么慢?""为什么改了 CSS 后页面闪一下?""DOM 操作怎么优化?" —— 这些问题的答案都藏在"浏览器渲染流水线"里。这篇文章把浏览器从拿到 HTML 到屏幕显示像素的完整流程讲透,让你看懂 Chrome DevTools Performance 面板的每一个 stage。

渲染流水线总览

HTML  ->  DOM 树
CSS   ->  CSSOM 树
        ↓
        Render 树(合并 DOM + CSSOM,只含可见元素)
        ↓
        Layout(布局,算每个元素的位置和大小)
        ↓
        Paint(绘制,把每个元素画到层上)
        ↓
        Composite(合成,把所有层叠起来送显示器)

每一步都可能成为性能瓶颈。理解它们,你就知道为什么"改 width 比改 transform 慢"。

解析 HTML 与构建 DOM

浏览器收到 HTML,从上到下 streaming 解析,边收边建 DOM 树。遇到 <script>(无 async / defer):

  • 暂停解析,下载并执行脚本。
  • 脚本执行完,继续解析。

这就是为什么"把 script 放底部"或"用 defer / async" —— 避免脚本阻塞解析。defer 是"下载并行,执行延迟到 DOM 解析完",async 是"下载并行,下载完就立刻执行(可能打断解析)"。一般 defer 更安全。

解析 CSS 与 CSSOM

CSS 是render-blocking —— 在 CSSOM 没建好之前,浏览器不会渲染(防止"无样式闪烁")。所以:

  • 关键 CSS 内联到 <head>
  • 非关键 CSS 用 <link rel="preload" as="style"> 异步加载。
  • media 属性把不同情况的 CSS 标记成不阻塞(media="print" 等)。

Layout:Reflow 重排

浏览器计算每个元素在视口里的几何位置和大小。一旦发生,所有受影响的子元素都要重新算。触发 Reflow 的常见操作:

// 改盒模型属性 -> Reflow
element.style.width = '300px';
element.style.padding = '10px';

// 改字体大小 -> Reflow(连父子元素都受影响)
element.style.fontSize = '20px';

// 添加 / 删除 DOM -> Reflow
parent.appendChild(child);

// 读取某些属性 -> 强制 Reflow!
const w = element.offsetWidth;     // 浏览器必须立刻 Layout 才能返回值

// 写 + 读混合 -> 频繁强制 Reflow(性能噩梦)
for (let i = 0; i < 100; i++) {
    items[i].style.width = items[i].offsetWidth + 10 + 'px';
}
// 这个循环触发 100 次 Reflow

// 优化:先读后写
const widths = items.map(i => i.offsetWidth);
items.forEach((item, i) => item.style.width = widths[i] + 10 + 'px');
// 只 1 次 Reflow

Paint:绘制

Layout 完成后,浏览器把每个元素的"视觉表现"画到栅格化的层(layer)上。颜色、边框、阴影、文字 都是 Paint 阶段做。

只改 Paint 不改 Layout 的属性(如 background-colorcolorbox-shadow)比改 Layout 快 —— 但还是涉及位图重画。

Composite:合成

页面可能有多个层(transform 元素、video、canvas、fixed 元素等),浏览器把它们按 z-index 叠起来送显示器。这一步是 GPU 做的,极快。

关键认知:仅触发 Composite 的属性最快:

  • transform:translate / scale / rotate。
  • opacity
  • filter(部分情况)。

所以"动画用 transform 而不是 left/top"是性能金科玉律 —— transform 不触发 Layout 和 Paint,只触发 Composite。FPS 能稳到 60。

关键渲染路径优化

从用户敲下 URL 到第一次内容渲染(First Contentful Paint,FCP)的优化:

  1. 减少 HTML / CSS / JS 大小:gzip / brotli 压缩,删未用代码(tree-shaking)。
  2. 关键资源 preload:浏览器尽早发现并下载。
  3. CSS 内联:关键 CSS 直接 <style><head>,不等外部文件。
  4. JS defer / async:别让 JS 阻塞解析。
  5. 图片 lazy load:首屏外的图加 loading="lazy"
  6. 用 HTTP/2 / HTTP/3:多路复用减少连接开销。

Core Web Vitals

Google 2020 提出的三大性能指标,SEO 和用户体验都看重:

  • LCP(Largest Contentful Paint):最大内容元素渲染完成的时间。< 2.5s 优秀。
  • FID(First Input Delay)/ INP(Interaction to Next Paint):用户首次交互到响应的延迟。2024 起 INP 替代 FID。< 200ms 优秀。
  • CLS(Cumulative Layout Shift):累计布局偏移。< 0.1 优秀。
# 用 web-vitals 库测量
import { onCLS, onINP, onLCP } from 'web-vitals';

onCLS(console.log);
onINP(console.log);
onLCP(console.log);

# 上报到分析平台
function report(metric) {
    fetch('/analytics', {
        method: 'POST',
        body: JSON.stringify(metric),
    });
}
onLCP(report);

调试工具:Chrome DevTools

Performance 面板录一段,你能看到:

  • Frames:每一帧的渲染时间(理想 16.6ms = 60 FPS)。
  • Main:主线程做了什么(JS 执行、Layout、Paint)。
  • Network:资源加载时序。
  • Layout Shifts:CLS 的具体触发源。

Performance Insights 面板(新版)更友好,直接告诉你"哪段时间在做什么、可以怎么优化"。

真实优化案例

长列表卡顿

渲染 1 万条数据,DOM 节点数过多,Layout / Paint 慢。解决:虚拟滚动(只渲染视口内的几十个)。React 用 react-window,Vue 用 vue-virtual-scroller。

滚动卡顿

常见原因:scroll 监听器里做了 Reflow 触发操作。解决:用 requestAnimationFrame 节流,只在每一帧做一次。

首屏慢

SPA 首次加载白屏几秒。解决:SSR / SSG / 流式渲染(下一篇详谈)。或者用 <link rel="preconnect"> 加速关键域名连接。

合成层(Compositing Layers)

不是所有元素都在同一层渲染。下面这些会触发"独立层":

  • transform 非 none,或 will-change: transform
  • opacity 不为 1。
  • position: fixed / sticky
  • video / canvas / WebGL。
  • filter(部分)。

独立层的好处:它的动画 / 重绘不影响其他层,GPU 直接合成。代价:每个层占显存(尤其大尺寸)。把动画元素提到独立层是优化技巧,但不要给所有元素加 will-change —— 几千个层比一个大层还慢。

主线程的瓶颈

渲染流水线大部分都在主线程跑,JS、Layout、Paint 都抢同一个线程。一个长 JS 任务(几百 ms)会让页面卡住。

// 长任务示例:在主线程做 1 亿次循环
function heavy() {
    let sum = 0;
    for (let i = 0; i < 1e8; i++) sum += i;
    return sum;
}
heavy();  // 主线程 1-2 秒,页面完全无响应

// 优化 1:任务分片
async function heavyChunked() {
    let sum = 0;
    for (let i = 0; i < 1e8; i++) {
        sum += i;
        if (i % 1e6 === 0) {
            await new Promise(resolve => setTimeout(resolve, 0));  // 让出主线程
        }
    }
    return sum;
}

// 优化 2:Web Worker
const worker = new Worker('heavy.js');
worker.postMessage({});
worker.onmessage = (e) => console.log(e.data);
// 计算在 Worker 线程,主线程不卡

requestAnimationFrame vs requestIdleCallback

// rAF:在下一帧渲染前执行 —— 适合视觉更新
requestAnimationFrame(() => {
    element.style.transform = 'translateX(100px)';
});

// rIC:空闲时执行 —— 适合低优先级任务(数据上报、预加载)
requestIdleCallback(() => {
    sendAnalytics(...);
}, { timeout: 2000 });

写在最后

浏览器渲染机制看似底层,但理解它能让你的前端代码从"能跑"升级到"跑得快"。每次卡顿的根源都能追溯到"哪一步触发了过多 Layout / Paint"。掌握这个模型,你看 DevTools 报告不再是猜,而是定向修复。下一篇我们看 V8 引擎是怎么让 JS 跑得这么快的。

一图看懂

浏览器渲染流水线一图看懂:

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

TLS 1.3 完全指南:从握手流程到 Let's Encrypt 实战

2026-5-15 17:32:38

技术教程

V8 引擎完全指南:从 Ignition 解释器到 TurboFan 优化

2026-5-15 17:32:39

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索