从初级到资深:多重境界彻底讲透浏览器渲染原理,吊打面试官

6 阅读1分钟

一、初级境界:理解基本流程

1.1 渲染流程概览

URL 输入
  ↓
DNS 解析
  ↓
建立 TCP 连接
  ↓
发送 HTTP 请求
  ↓
接收 HTML
  ↓
解析 HTML → DOM 树
  ↓
解析 CSS → CSSOM 树
  ↓
合并 → Render 树
  ↓
布局(Layout)
  ↓
绘制(Paint)
  ↓
合成(Composite)
  ↓
显示页面

1.2 关键步骤详解

步骤 1:构建 DOM 树

<!DOCTYPE html>
<html>
  <head>
    <title>Page</title>
  </head>
  <body>
    <div id="app">
      <h1>Hello</h1>
      <p>World</p>
    </div>
  </body>
</html>

浏览器解析成 DOM 树:

Document
└── html
    ├── head
    │   └── title
    │       └── "Page"
    └── body
        └── div#app
            ├── h1
            │   └── "Hello"
            └── p
                └── "World"

步骤 2:构建 CSSOM 树

body { font-size: 16px; }
#app { color: blue; }
h1 { font-size: 24px; }

解析成 CSSOM 树:

body { font-size: 16px }
└── #app { color: blue }
    └── h1 { font-size: 24px }

步骤 3:合并成 Render 树

DOM + CSSOM = Render 树

RenderObject(body)
└── RenderObject(div#app)
    ├── RenderObject(h1) { font-size: 24px, color: blue }
    └── RenderObject(p) { font-size: 16px, color: blue }

步骤 4:布局(Layout)

计算每个元素的位置和大小:

h1: { x: 0, y: 0, width: 800, height: 24 }
p:  { x: 0, y: 24, width: 800, height: 16 }

步骤 5:绘制(Paint)

将元素绘制成像素:

绘制 h1 的背景
绘制 h1 的文字
绘制 p 的背景
绘制 p 的文字

步骤 6:合成(Composite)

将多个图层合成最终图像。

二、中级境界:理解重排和重绘

2.1 重排(Reflow)

定义:元素的几何属性变化,需要重新计算布局。

触发条件: - 添加/删除元素 - 修改元素尺寸(width、height、padding、margin) - 修改元素位置(top、left) - 修改字体大小 - 窗口大小变化 - 读取某些属性(offsetWidth、scrollTop)

示例

// ❌ 触发重排
element.style.width = '100px';
element.style.height = '100px';
element.style.margin = '10px';

// 每次修改都触发一次重排,共 3 次

优化

// ✅ 批量修改
element.style.cssText = 'width: 100px; height: 100px; margin: 10px;';

// 或使用 class
element.className = 'new-style';

2.2 重绘(Repaint)

定义:元素的外观变化,但不影响布局。

触发条件: - 修改颜色(color、background-color) - 修改可见性(visibility) - 修改边框样式(border-style)

示例

// 只触发重绘,不触发重排
element.style.color = 'red';
element.style.backgroundColor = 'blue';

2.3 重排 vs 重绘

操作重排重绘性能
修改 width
修改 color
修改 transform最快

性能对比

重排 + 重绘 > 重绘 > 合成

三、高级境界:性能优化

3.1 避免强制同步布局

问题代码

// ❌ 强制同步布局
for (let i = 0; i < 100; i++) {
  const width = element.offsetWidth;  // 读取,触发布局
  element.style.width = width + 10 + 'px';  // 写入,触发重排
}
// 读写交替,触发 100 次重排

优化

// ✅ 批量读取,批量写入
const width = element.offsetWidth;  // 一次读取
for (let i = 0; i < 100; i++) {
  element.style.width = width + 10 * i + 'px';
}
// 只触发 1 次重排

3.2 使用 transform 代替 top/left

问题代码

// ❌ 触发重排
element.style.left = '100px';
element.style.top = '100px';

优化

// ✅ 只触发合成
element.style.transform = 'translate(100px, 100px)';

原因transform 在合成层处理,不触发重排和重绘。

3.3 使用 will-change 提示浏览器

.animated {
  will-change: transform, opacity;
}

作用:告诉浏览器这个元素会变化,提前优化。

注意:不要滥用,会消耗内存。

3.4 使用 requestAnimationFrame

// ❌ 使用 setTimeout
setTimeout(() => {
  element.style.left = '100px';
}, 16);

// ✅ 使用 requestAnimationFrame
requestAnimationFrame(() => {
  element.style.left = '100px';
});

优势: - 与浏览器刷新率同步 - 页面不可见时自动暂停 - 性能更好

3.5 虚拟滚动

问题:渲染 10000 个列表项,性能差。

// ❌ 渲染所有项
for (let i = 0; i < 10000; i++) {
  list.appendChild(createItem(i));
}

优化:只渲染可见区域

// ✅ 虚拟滚动
const visibleStart = Math.floor(scrollTop / itemHeight);
const visibleEnd = visibleStart + visibleCount;

for (let i = visibleStart; i < visibleEnd; i++) {
  list.appendChild(createItem(i));
}

四、资深境界:深入原理

4.1 渲染层合成

图层类型

1. 普通图层:默认图层 2. 合成图层:独立图层,GPU 加速

创建合成图层的条件

/* 1. 3D transform */
transform: translateZ(0);
transform: translate3d(0, 0, 0);

/* 2. will-change */
will-change: transform;

/* 3. video、canvas、iframe */

/* 4. position: fixed */
position: fixed;

/* 5. opacity 动画 */
@keyframes fade {
  from { opacity: 0; }
  to { opacity: 1; }
}

优势: - GPU 加速 - 不影响其他图层 - 动画更流畅

劣势: - 消耗内存 - 图层过多反而变慢

4.2 关键渲染路径优化

目标:尽快显示首屏内容。

优化策略

1. 减少关键资源数量

<!-- ❌ 阻塞渲染 -->
<link rel="stylesheet" href="style.css">
<script src="app.js"></script>

<!-- ✅ 异步加载 -->
<link rel="stylesheet" href="style.css" media="print" onload="this.media='all'">
<script src="app.js" defer></script>

2. 减少关键资源大小

# 压缩 CSS
cssnano style.css -o style.min.css

# 压缩 JS
terser app.js -o app.min.js

# Gzip 压缩
gzip -9 style.min.css

3. 缩短关键路径长度

<!-- ❌ 串行加载 -->
<link rel="stylesheet" href="a.css">
<link rel="stylesheet" href="b.css">

<!-- ✅ 内联关键 CSS -->
<style>
  /* 首屏关键样式 */
</style>
<link rel="stylesheet" href="non-critical.css" media="print" onload="this.media='all'">

4.3 渲染性能指标

FCP(First Contentful Paint):首次内容绘制

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('FCP:', entry.startTime);
  }
}).observe({ entryTypes: ['paint'] });

LCP(Largest Contentful Paint):最大内容绘制

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('LCP:', entry.startTime);
  }
}).observe({ entryTypes: ['largest-contentful-paint'] });

FID(First Input Delay):首次输入延迟

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('FID:', entry.processingStart - entry.startTime);
  }
}).observe({ entryTypes: ['first-input'] });

CLS(Cumulative Layout Shift):累积布局偏移

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('CLS:', entry.value);
  }
}).observe({ entryTypes: ['layout-shift'] });

五、面试高频问题

5.1 从输入 URL 到页面显示发生了什么?

完整流程

1. DNS 解析:域名 → IP 2. TCP 连接:三次握手 3. 发送 HTTP 请求 4. 服务器处理请求 5. 返回 HTTP 响应 6. 浏览器解析 HTML 7. 构建 DOM 树 8. 解析 CSS,构建 CSSOM 树 9. 合并成 Render 树 10. 布局(Layout) 11. 绘制(Paint) 12. 合成(Composite) 13. 显示页面

5.2 重排和重绘的区别?

重排: - 元素几何属性变化 - 需要重新计算布局 - 性能开销大

重绘: - 元素外观变化 - 不影响布局 - 性能开销小

优化: - 批量修改样式 - 使用 class 代替 style - 使用 transform 代替 top/left - 避免强制同步布局

5.3 如何优化渲染性能?

策略

1. 减少重排重绘 - 批量修改 DOM - 使用 transform - 使用 DocumentFragment

2. 优化关键渲染路径 - 内联关键 CSS - 异步加载 JS - 压缩资源

3. 使用合成层 - transform - will-change - GPU 加速

4. 虚拟滚动 - 只渲染可见区域 - 减少 DOM 数量

5. 懒加载 - 图片懒加载 - 路由懒加载 - 组件懒加载

六、总结

初级境界: - 理解渲染流程 - 知道 DOM、CSSOM、Render 树 - 了解布局、绘制、合成

中级境界: - 理解重排和重绘 - 知道触发条件 - 会基本优化

高级境界: - 避免强制同步布局 - 使用 transform - 使用 requestAnimationFrame - 虚拟滚动

资深境界: - 理解渲染层合成 - 优化关键渲染路径 - 掌握性能指标 - 深入浏览器原理

面试要点: - 完整的渲染流程 - 重排和重绘的区别 - 性能优化策略 - 实际案例分析

掌握这些知识,你就能在面试中游刃有余,吊打面试官。

如果这篇文章对你有帮助,欢迎点赞收藏。