DOM 性能与渲染
系列文章目录
《JavaScript 基础与进阶笔记》(前期偏基础巩固与常见面试点,后续进入闭包、异步、工程化等进阶主题)
- 第 01 篇:数据类型与类型判断
- 第 02 篇:变量声明与作用域
- 第 03 篇:闭包与高阶函数
- 第 04 篇:函数工厂
- 第 05 篇:this 指向与绑定
- 第 06 篇:原型与原型链
- 第 07 篇:类与继承
- 第 08 篇:JS 执行机制与异步队列
- 第 09 篇:数组常用方法
- 第 10 篇:字符串算法
- 第 11 篇:常见手写题合集(上)
- 第 12 篇:常见手写题合集(下)
- 第 13 篇:Promise 与 async/await
- 第 14 篇:数据结构基础
- 第 15 篇:垃圾回收与内存
- 第 16 篇:DOM 基础全面解析
- 第 17 篇:DOM 性能与渲染(本文)
文章目录
- 系列文章目录
- 前言
- 一、浏览器渲染主流程
- 1.1 各阶段简述
- 1.2 DOM 树 vs 渲染树
- 二、Reflow、Repaint、Composite
- 2.1 定义与关系
- 2.2 常见触发(口述用)
- 2.3 `display: none` vs `visibility: hidden`
- 三、渲染队列与强制同步布局
- 3.1 反模式:读写交替
- 3.2 优化:批量写、批量读
- 四、DOM 批量操作与 DocumentFragment
- 五、CSS / JS 与首屏:阻塞与 FOUC
- 5.1 CSS
- 5.2 JavaScript
- 5.3 与性能指标(了解)
- 六、动画:`transform` / `opacity` 与 `will-change`
- 6.1 为何 `transform` / `opacity` 更友好
- 6.2 `will-change`(慎用)
- 6.3 `requestAnimationFrame`(预告)
- 七、优化清单(速查)
- 八、易混淆点归纳
- 九、思考与练习
- 总结
前言
第 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 定义与关系
- 回流(Reflow / Layout)— 几何属性(宽高、位置、display 等)变化,需重新计算布局;开销最大。
- 重绘(Repaint)— 外观变化(颜色、背景、
visibility等)不影响布局,只重画像素;开销次之。 - 合成(Composite)— 已有图层上变换(如
transform、opacity),常由GPU完成;开销相对最小。
关系:回流一般会触发重绘;重绘不一定回流;合成通常不触发布局(在独立合成层上时)。
2.2 常见触发(口述用)
易触发回流:
- 增删 DOM、改变盒模型(
width/height/margin/padding) - 读写
offsetWidth、clientHeight、getBoundingClientRect()等(见下节) - 窗口 resize、字体加载改变度量
display: none↔ 其他 display
易触发重绘(不一定回流):
color、background、box-shadowvisibility: hidden(保留布局)
倾向只走合成:
transform(translate、scale、rotate)opacity(在 promoted 层上时)
constel=document.querySelector(".box");// 易触发 Layout + Paintel.style.width="200px";el.style.left="100px";// 动画更友好:Compositeel.style.transition="transform 0.3s";el.style.transform="translateX(100px)";2.3display: nonevsvisibility: hidden
| 属性 | 渲染树 | 占布局空间 | 典型触发 |
|---|---|---|---|
display: none | 移除 | 否 | 回流 |
visibility: hidden | 保留 | 是 | 重绘 |
三、渲染队列与强制同步布局
浏览器会把多次样式变更批量进渲染队列,在合适时机(如帧末)统一 Layout/Paint,避免每改一行 CSS 就全页算一遍。
读取以下属性会强制刷新队列(必须先算出最新布局),称为强制同步布局(Forced Synchronous Layout / Layout Thrashing):
offsetWidth/offsetHeight/offsetTop/offsetLeftclientWidth/clientHeightscrollWidth/scrollHeight/scrollTopgetComputedStyle(...)getBoundingClientRect()
3.1 反模式:读写交替
constel=document.querySelector(".box");// ❌ 每次循环:写 style → 读 offsetWidth → 强制回流for(leti=0;i<100;i++){el.style.width=`${i}px`;console.log(el.offsetWidth);}3.2 优化:批量写、批量读
// ✅ 先批量写constpositions=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在内存中组好子树,一次插入:
constul=document.querySelector("ul");constfrag=document.createDocumentFragment();for(leti=0;i<500;i++){constli=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/opacity与will-change
6.1 为何transform/opacity更友好
- 改变
top/left/width常触发Layout。 transform、opacity可在合成层上由 GPU 处理,跳过主线程 Layout(在层已提升且仅合成属性变化时)。
constcard=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.2will-change(慎用)
.card{will-change:transform;}- 作用:提前告知浏览器某属性将变,可能创建独立合成层。
- 风险:层过多占GPU 内存;长期开启反而浪费。
- 建议:动画开始前加、结束后移除;不要对大量元素默认
will-change: transform。
6.3requestAnimationFrame(预告)
动画循环应用rAF对齐刷新率,而非setTimeout(16);滚动节流、后台标签暂停等见第 20 篇。
七、优化清单(速查)
| 问题 | 方向 |
|---|---|
| 频繁回流 | 合并 DOM 写操作、DocumentFragment、虚拟列表 |
| Layout Thrashing | 读写分离,避免循环内读 layout |
| 动画卡顿 | transform/opacity,少改 layout 属性 |
| 首屏 FOUC | CSS 提前、关键 CSS 内联或 preload |
| 解析阻塞 | scriptdefer/async |
| 层爆炸 | will-change按需、用完即删 |
八、易混淆点归纳
- 回流 ⊃ 重绘(几何变必画);重绘不一定回流。
- Composite 不是零成本,但通常比全页 Layout 轻。
- 读
offset*会强制布局,与是否刚写过 style 有关。 - CSS 阻塞渲染、不阻塞 DOM;JS 默认阻塞解析。
visibility: hidden回流?— 一般重绘为主;display: none回流。- 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/opacity;will-change慎用。 - 加载:CSS 放 head 减 FOUC;JS 用defer/async减阻塞(见第 16 篇)。
下一篇讲事件系统:捕获/冒泡、targetvscurrentTarget、委托与passive等。
