系列文章目录

《JavaScript 基础与进阶笔记》(前期偏基础巩固与常见面试点,后续进入闭包、异步、工程化等进阶主题)



前言

第 16 篇讲了 DOM 节点与 API;本篇回答「改 DOM 之后浏览器在干什么」以及「怎样少卡顿」。核心线索:渲染流水线(DOM → CSSOM → 渲染树 → Layout → Paint → Composite)、回流/重绘/合成 的开销差异、强制同步布局 的陷阱,以及 transform / opacity 动画DocumentFragment 批量操作 等优化手段。CSS/JS 阻塞与 FOUC 与第 16 篇脚本部分衔接,此处从渲染视角补全。


一、浏览器渲染主流程

从输入 URL 到像素上屏,与前端性能最相关的一段可简化为:

HTML 解析 → DOM 树
                ↘
CSS 解析  → CSSOM 树 → 渲染树(Render Tree)→ Layout(布局/回流)
                                              → Paint(绘制/重绘)
                                              → Composite(合成)

1.1 各阶段简述

阶段 产出 说明
DOM DOM 树 HTML 解析;第 16 篇
CSSOM CSS 对象模型 外部/内联 CSS 解析
Render Tree 渲染树 DOM + 样式;不含 display: none 等不可见节点
Layout 几何信息 计算位置与尺寸;也称 Reflow(回流)
Paint 绘制记录 填充颜色、文字、阴影等;Repaint(重绘)
Composite 图层合成 GPU 合并层,输出屏幕

注意:现代浏览器会对步骤合并与跳过(如仅改 transform 可能跳过 Layout/Paint),下节按概念模型理解即可。

1.2 DOM 树 vs 渲染树

DOM 树 渲染树
display: none 不在
visibility: hidden (占空间,不可见)
<head> 内 meta 等 通常不在(无盒)

二、Reflow、Repaint、Composite

中文常称 回流(Reflow)Layout重绘(Repaint)Paint

2.1 定义与关系

  1. 回流(Reflow / Layout) — 几何属性(宽高、位置、display 等)变化,需重新计算布局;开销最大
  2. 重绘(Repaint) — 外观变化(颜色、背景、visibility 等)不影响布局,只重画像素;开销次之。
  3. 合成(Composite) — 已有图层上变换(如 transformopacity),常由 GPU 完成;开销相对最小

关系:回流 一般 会触发重绘;重绘 不一定 回流;合成 通常 不触发布局(在独立合成层上时)。

2.2 常见触发(口述用)

易触发回流

  • 增删 DOM、改变盒模型(width/height/margin/padding
  • 读写 offsetWidthclientHeightgetBoundingClientRect() 等(见下节)
  • 窗口 resize、字体加载改变度量
  • display: none ↔ 其他 display

易触发重绘(不一定回流)

  • colorbackgroundbox-shadow
  • visibility: hidden(保留布局)

倾向只走合成

  • transformtranslatescalerotate
  • opacity(在 promoted 层上时)
const el = document.querySelector(".box");

// 易触发 Layout + Paint
el.style.width = "200px";
el.style.left = "100px";

// 动画更友好:Composite
el.style.transition = "transform 0.3s";
el.style.transform = "translateX(100px)";

2.3 display: none vs visibility: hidden

属性 渲染树 占布局空间 典型触发
display: none 移除 回流
visibility: hidden 保留 重绘

三、渲染队列与强制同步布局

浏览器会把多次样式变更批量渲染队列,在合适时机(如帧末)统一 Layout/Paint,避免每改一行 CSS 就全页算一遍。

读取以下属性会强制刷新队列(必须先算出最新布局),称为 强制同步布局(Forced Synchronous Layout / Layout Thrashing)

  • offsetWidth / offsetHeight / offsetTop / offsetLeft
  • clientWidth / clientHeight
  • scrollWidth / scrollHeight / scrollTop
  • getComputedStyle(...)
  • getBoundingClientRect()

3.1 反模式:读写交替

const el = document.querySelector(".box");

// ❌ 每次循环:写 style → 读 offsetWidth → 强制回流
for (let i = 0; i < 100; i++) {
  el.style.width = `${i}px`;
  console.log(el.offsetWidth);
}

3.2 优化:批量写、批量读

// ✅ 先批量写
const positions = Array.from({ length: 100 }, (_, i) => i);
positions.forEach((i) => {
  el.style.width = `${i}px`;
});
// 再读一次
console.log(el.offsetWidth);

原则先写后读读写分离;无法分离时用 requestAnimationFrame 把读放到下一帧(第 20 篇详述 rAF)。


四、DOM 批量操作与 DocumentFragment

多次 appendChild 到同一父节点,可能触发多次回流。先用 DocumentFragment 在内存中组好子树,一次插入:

const ul = document.querySelector("ul");
const frag = document.createDocumentFragment();

for (let i = 0; i < 500; i++) {
  const li = document.createElement("li");
  li.textContent = `item ${i}`;
  frag.appendChild(li);
}

ul.appendChild(frag); // 一次插入,减少 Layout 次数

其他手段

  • display: none 或离屏容器上改完再挂回(旧技巧,慎用可访问性)
  • 虚拟列表(第 12 篇):控制 DOM 节点总数
  • 框架的 批量更新(Virtual DOM diff 后一次 patch)

五、CSS / JS 与首屏:阻塞与 FOUC

5.1 CSS

  • 不阻塞 DOM 解析(HTML 继续建 DOM)。
  • 阻塞渲染:浏览器倾向在 CSSOM 就绪前不绘制,避免 FOUC(Flash of Unstyled Content,无样式内容闪烁)
  • 实践:关键 CSS 放 <head> 尽早加载;非关键 CSS 可异步或按需。

5.2 JavaScript

  • 默认 <script> 阻塞 HTML 解析(第 16 篇)。
  • defer:并行下载,DOM 解析完再按序执行。
  • async:下载完即执行,不保证顺序。

5.3 与性能指标(了解)

  • FCP:首次有内容绘制。
  • LCP:最大内容块绘制(Core Web Vitals 之一)。
  • 阻塞渲染的资源越晚、越少,首屏通常越好(还需结合体积、CDN、缓存等)。

六、动画:transform / opacitywill-change

6.1 为何 transform / opacity 更友好

  • 改变 top/left/width 常触发 Layout
  • transformopacity 可在合成层上由 GPU 处理,跳过主线程 Layout(在层已提升且仅合成属性变化时)。
const card = document.querySelector(".card");
card.style.transition = "transform 0.3s ease, opacity 0.3s";
card.style.transform = "translateY(-8px)";
card.style.opacity = "0.95";

移动元素优先 translate,而非 top/left

6.2 will-change(慎用)

.card {
  will-change: transform;
}
  • 作用:提前告知浏览器某属性将变,可能创建独立合成层
  • 风险:层过多占 GPU 内存;长期开启反而浪费。
  • 建议:动画开始前加、结束后移除;不要对大量元素默认 will-change: transform

6.3 requestAnimationFrame(预告)

动画循环应用 rAF 对齐刷新率,而非 setTimeout(16);滚动节流、后台标签暂停等见第 20 篇。


七、优化清单(速查)

问题 方向
频繁回流 合并 DOM 写操作、DocumentFragment、虚拟列表
Layout Thrashing 读写分离,避免循环内读 layout
动画卡顿 transform / opacity,少改 layout 属性
首屏 FOUC CSS 提前、关键 CSS 内联或 preload
解析阻塞 script defer/async
层爆炸 will-change 按需、用完即删

八、易混淆点归纳

  1. 回流 ⊃ 重绘(几何变必画);重绘不一定回流
  2. Composite 不是零成本,但通常比全页 Layout 轻。
  3. offset* 会强制布局,与是否刚写过 style 有关。
  4. CSS 阻塞渲染、不阻塞 DOMJS 默认阻塞解析
  5. visibility: hidden 回流? — 一般重绘为主;display: none 回流
  6. rAF 不是微任务也不是宏任务,在渲染前回调(第 08、20 篇对照)。

九、思考与练习

1. 只改 background-color,会回流吗?

解析:通常不触发布局,主要 重绘

2. 循环 100 次 el.style.left = i + 'px' 且每次读 offsetWidth,为何慢?

解析:每次读 layout 属性强制同步布局,导致多次回流

3. DocumentFragment 插入后,fragment 里的节点去哪了?

解析:插入时子节点移到真实父节点,fragment 变空,可复用或丢弃。

4. 为何移动端大量 will-change: transform 可能更卡?

解析:合成层过多占 GPU 内存,合成本身也有开销。

5. defer 脚本与 DOMContentLoaded 谁先?

解析:defer 脚本按序执行完后,再触发 DOMContentLoaded(无其他阻塞时)。


总结

  • 流水线:DOM + CSSOM → 渲染树Layout(回流)Paint(重绘)Composite(合成)
  • 开销:回流 > 重绘 > 合成(概念上);避免 读写交替 造成强制同步布局。
  • 优化DocumentFragment 批量 DOM;动画用 transform/opacitywill-change 慎用
  • 加载:CSS 放 head 减 FOUC;JS 用 defer/async 减阻塞(见第 16 篇)。

下一篇讲 事件系统:捕获/冒泡、target vs currentTarget、委托与 passive 等。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐