浏览器渲染原理?

浏览器渲染原理

浏览器把 HTML/CSS/JS 变成你看到的页面,整体流程可以分为以下几个核心阶段。


完整渲染流水线

复制代码
HTML/CSS/JS
    ↓
解析 HTML → DOM 树
解析 CSS  → CSSOM 树
    ↓
合并 → Render Tree(渲染树)
    ↓
Layout(布局/回流)
    ↓
Paint(绘制/重绘)
    ↓
Composite(合成)
    ↓
显示到屏幕

一、解析阶段

构建 DOM 树

复制代码
<!-- 原始 HTML -->
<html>
  <body>
    <div class="box">
      <p>Hello</p>
    </div>
  </body>
</html>

浏览器逐字节读取 HTML,经过字节 → 字符 → Token → Node → DOM 的转化过程:

复制代码
DOM Tree:
html
  └─ body
       └─ div.box
            └─ p
                 └─ "Hello"

重要细节

  • 解析是边下载边解析,不等全部下载完

  • 遇到 <script>阻塞解析(因为 JS 可能修改 DOM)

  • asyncdefer 可以避免阻塞:

    <script src="main.js"></script> <script async src="main.js"></script> <script defer src="main.js"></script>

构建 CSSOM 树

复制代码
div { font-size: 16px; }
.box { color: red; }
.box p { font-weight: bold; }

CSSOM Tree:
div
  └─ font-size: 16px

.box
  └─ color: red
  └─ p
       └─ font-weight: bold

重要细节

  • CSS 解析会阻塞渲染(有了 CSSOM 才能合并渲染树)
  • CSS 不阻塞 DOM 解析,但阻塞 JS 执行(JS 可能读取样式)
  • 所以 CSS 要放 <head> 里,JS 放 <body> 底部

二、Render Tree(渲染树)

把 DOM 树和 CSSOM 树合并,只保留可见节点

复制代码
// 这些节点不会进入 Render Tree
display: none        // 完全不渲染
<head>、<script>     // 不可见元素
<!-- 注释 -->        // 注释节点

// 这个会进入 Render Tree(虽然不可见,但占位置)
visibility: hidden   // 占据空间,只是不可见

Render Tree:
body
  └─ div.box (color: red, font-size: 16px)
       └─ p (font-weight: bold)
            └─ "Hello"

三、Layout(布局 / 回流)

计算每个节点的位置和大小

复制代码
// Layout 需要计算的信息
{
  x: 0,
  y: 0,
  width: 1200,
  height: 400
}

触发回流(Reflow)的操作

复制代码
// 改变几何属性 → 触发回流(代价最大!)
element.style.width = '200px';
element.style.height = '300px';
element.style.margin = '10px';
element.style.padding = '5px';
element.style.fontSize = '20px';

// 读取几何信息 → 强制同步回流(非常慢!)
const width = element.offsetWidth;
const height = element.offsetHeight;
const rect = element.getBoundingClientRect();

// 添加/删除 DOM → 触发回流
document.body.appendChild(newDiv);

回流优化技巧

复制代码
// ❌ 错误:每次修改都触发回流
element.style.width = '200px';
element.style.height = '300px';
element.style.margin = '10px';

// ✅ 正确:批量修改(只触发一次回流)
element.style.cssText = 'width: 200px; height: 300px; margin: 10px;';
// 或者
element.className = 'new-class';

// ❌ 错误:读写交替(触发多次强制同步回流)
const width1 = el1.offsetWidth;  // 读 → 强制回流
el1.style.width = '100px';       // 写
const width2 = el2.offsetWidth;  // 读 → 再次强制回流
el2.style.width = '200px';       // 写

// ✅ 正确:先读后写(只触发一次回流)
const width1 = el1.offsetWidth;  // 读
const width2 = el2.offsetWidth;  // 读
el1.style.width = '100px';       // 写
el2.style.width = '200px';       // 写

// ✅ 使用 DocumentFragment 批量插入 DOM
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.textContent = `Item ${i}`;
  fragment.appendChild(li);  // 操作 fragment,不触发回流
}
document.body.appendChild(fragment);  // 只触发一次回流

四、Paint(绘制 / 重绘)

把每个节点绘制成像素,包括颜色、边框、阴影、文字等。

触发重绘(Repaint)的操作

复制代码
// 只改变外观,不影响布局 → 只触发重绘(比回流轻量)
element.style.color = 'red';
element.style.background = 'blue';
element.style.borderColor = 'green';
element.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)';
element.style.visibility = 'hidden';

回流 vs 重绘

复制代码
回流(Reflow):
几何属性变化 → Layout → Paint → Composite
代价:最大

重绘(Repaint):
外观属性变化 → Paint → Composite
代价:中等

合成(Composite):
transform/opacity → 只 Composite
代价:最小 ✅

五、Composite(合成)

浏览器把页面分成多个图层(Layer),分别绘制后再合成:

复制代码
// 触发独立图层(GPU 加速)
transform: translateZ(0)       // 强制提升为独立图层
will-change: transform         // 提前告知浏览器
position: fixed                // 固定定位
opacity < 1 + animation        // 有动画的透明度
video、canvas、iframe          // 自动提升

// 只触发 Composite 的属性(最高效!)
transform: translate(100px, 0)
transform: scale(1.5)
opacity: 0.5

动画最佳实践

复制代码
/* ❌ 触发回流的动画(卡顿)*/
.bad-animation {
  transition: left 0.3s, top 0.3s;  /* left/top 触发回流 */
}

/* ✅ 只触发 Composite 的动画(流畅)*/
.good-animation {
  transition: transform 0.3s;  /* transform 只走合成 */
  will-change: transform;      /* 提前提升图层 */
}

六、关键渲染路径优化

优化策略汇总

复制代码
// 1. CSS 放 <head>,JS 放底部 或用 defer
<head>
  <link rel="stylesheet" href="style.css">  // ✅
</head>
<body>
  <!-- 内容 -->
  <script defer src="app.js"></script>      // ✅
</body>

// 2. 避免强制同步回流
// ❌
function animate() {
  box.style.left = box.offsetLeft + 1 + 'px';  // 读写混合!
}

// ✅
let position = 0;
function animate() {
  position += 1;
  box.style.transform = `translateX(${position}px)`;  // 用 transform
}

// 3. 使用 requestAnimationFrame 做动画
function animate() {
  element.style.transform = `translateX(${x++}px)`;
  requestAnimationFrame(animate);  // 跟随屏幕刷新率,不丢帧
}
requestAnimationFrame(animate);

// 4. 复杂计算放 Web Worker(不阻塞主线程)
const worker = new Worker('heavy-task.js');
worker.postMessage({ data: bigData });
worker.onmessage = (e) => {
  console.log('计算结果:', e.data);
};

// 5. 大列表用虚拟滚动
// 只渲染可见区域的 DOM 节点
// 推荐:react-window、vue-virtual-scroller

七、浏览器的优化机制

渲染队列(异步合批)

复制代码
// 浏览器不会立即回流,而是把操作放入队列,统一处理
element.style.width = '100px';   // 进队列
element.style.height = '200px';  // 进队列
element.style.margin = '10px';   // 进队列
// 一次性触发回流 ✅

// 但读取几何属性会强制清空队列,立即回流
element.style.width = '100px';     // 进队列
const h = element.offsetHeight;    // 强制清空队列,立即回流 ❌
element.style.height = '200px';    // 再进队列,再次回流
相关推荐
小白探索世界欧耶!~1 小时前
Vue2项目引入sortablejs实现表格行拖曳排序
前端·javascript·vue.js·经验分享·elementui·html·echarts
叫我一声阿雷吧3 小时前
JS实现响应式导航栏(移动端汉堡菜单)|适配多端+无缝交互【附完整源码】
开发语言·javascript·交互
GISer_Jing3 小时前
前端营销(AIGC II)
前端·react.js·aigc
NEXT063 小时前
深度解析 JWT:从 RFC 原理到 NestJS 实战与架构权衡
前端·typescript·nestjs
程序员林北北4 小时前
【前端进阶之旅】节流与防抖:前端性能优化的“安全带”与“稳定器”
前端·javascript·vue.js·react.js·typescript
寻星探路5 小时前
【前端基础】HTML + CSS + JavaScript 快速入门(三):JS 与 jQuery 实战
java·前端·javascript·css·c++·ai·html
未来之窗软件服务6 小时前
未来之窗昭和仙君(六十九)前端收银台行为异常检测—东方仙盟练气
前端·仙盟创梦ide·东方仙盟·昭和仙君
大叔编程奋斗记6 小时前
两个日期间的相隔年月计算
前端·salesforce
上海合宙LuatOS7 小时前
LuatOS核心库API——【io】 io操作(扩展)
java·服务器·前端·网络·单片机·嵌入式硬件·物联网