深入理解现代前端框架基石:虚拟 DOM 与 Diff 算法
在学习 React 或 Vue 等现代前端框架时,“虚拟 DOM(Virtual DOM)”和“Diff 算法”是绕不开的核心概念。许多开发者可能背诵过“虚拟 DOM 性能高”的面经,但一旦被追问“原生 JS 操作 DOM 究竟重在哪里”或“Diff 算法具体如何优化性能”时,往往语焉不详。
本文将剥离框架的表层 API,从浏览器底层渲染机制出发,彻底剖析原生 DOM 的性能痛点,并推演虚拟 DOM 与 Diff 算法的破局之道。
一、 真实 DOM 的“不可承受之重”
在探讨虚拟 DOM 之前,我们必须先理解原生 DOM 操作为何被称为“极其笨重”。这种笨重主要体现在三个维度:内存占用、跨引擎通信以及渲染管线的代价。
1. 内存级别的笨重:庞大的原型链
在 HTML 中书写一个极其简单的 <div> 标签,当它被浏览器解析为真实的 DOM 节点时,绝不仅仅是一个单纯的文本节点。它在浏览器底层对应着一个复杂的 C++ 对象,并在 JavaScript 环境中映射为拥有极长原型链的实体。
我们可以通过下面的类图来直观感受一个真实 <div> 节点的继承关系:

即便是一个空的 <div>,其身上也默认挂载了接近 300 个属性和方法。在 V8 引擎中,这样一个真实的 DOM 节点大约需要占用 1KB 到 2KB 的内存。如果页面中存在十万个节点,仅 DOM 树本身就会占用上百兆的内存。频繁地销毁和重建这样庞大的对象,对浏览器的垃圾回收机制(GC)是一场灾难。
2. 跨界通信的“过路费”
浏览器架构中,JavaScript 引擎(如 V8)和渲染引擎(如 WebCore)是两个独立的线程。通过原生 JS 操作 DOM(例如 document.getElementById().style.color = 'red'),本质上是 JS 引擎在调用渲染引擎提供的 API。这种跨线程的通信成本极高。频繁的 DOM 操作意味着频繁跨越这座“桥梁”,性能会在此过程中被严重消耗。
3. 性能杀手:重排(Reflow)与重绘(Repaint)
这是原生 DOM 操作最致命的痛点。当 DOM 节点的几何属性(如宽高、位置)发生改变,或节点被增删时,浏览器必须重新计算该节点及其受影响的所有子节点、兄弟节点的布局,这被称为重排(Reflow)。
如果不加控制,连续 10 次的 DOM 插入操作可能会触发浏览器 10 次昂贵的重排,导致极其严重的“布局抖动(Layout Thrashing)”,使得页面出现明显的卡顿。
二、 破局者:虚拟 DOM (Virtual DOM)
理解了真实 DOM 的沉重,虚拟 DOM 的出现便显得顺理成章。
虚拟 DOM 本质上是一个非常轻量级的纯 JavaScript 对象(Plain Object),它是对真实 DOM 结构的一种抽象描述。
// 一个真实的 <div id="box">Hello</div> 对应的虚拟 DOM 大致如下:
const vNode = {
tag: 'div',
props: { id: 'box' },
children: ['Hello']
}
相比于动辄占用 1~2KB 内存的真实 DOM,这样一个精简的 JS 对象在内存中仅占用约 40 到 80 字节。两者的内存体积相差数十倍。
虚拟 DOM 的核心价值并不在于它本身执行速度有多快,而在于它提供了一个极其低成本的“内存草稿本”。 在这个草稿本上修改数据,不会触发任何浏览器的重排和重绘,也不会产生跨引擎的通信开销。
三、 核心引擎:Diff 算法
如果只有虚拟 DOM,每次数据更新都根据新的虚拟 DOM 生成完整的真实 DOM,性能依然无法提升。这就需要引入 Diff 算法。
Diff 算法的作用是:当状态发生改变时,对比“更新前”和“更新后”的两棵虚拟 DOM 树,精准地找出它们之间的差异。
传统的树对比算法时间复杂度高达 O(n3)O(n^3)O(n3),这在前端显然是不可接受的。React 等框架通过制定三大策略,将时间复杂度硬生生降到了 O(n)O(n)O(n):
四、 终极奥义:批处理(Batching)与最小化更新
现在,我们将虚拟 DOM 与 Diff 算法结合起来,揭示其提升性能的终极机制——批处理(Batching)。
这是导致许多开发者“恍然大悟”的核心环节。请看以下的渲染流程图:
浏览器渲染引擎 (真实 DOM)Diff 算法JS 内存 (虚拟 DOM)业务状态 (State)浏览器渲染引擎 (真实 DOM)Diff 算法JS 内存 (虚拟 DOM)业务状态 (State)#mermaid-svg-euXcS4Rrs93RwVkQ{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-euXcS4Rrs93RwVkQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-euXcS4Rrs93RwVkQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-euXcS4Rrs93RwVkQ .error-icon{fill:#552222;}#mermaid-svg-euXcS4Rrs93RwVkQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-euXcS4Rrs93RwVkQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-euXcS4Rrs93RwVkQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-euXcS4Rrs93RwVkQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-euXcS4Rrs93RwVkQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-euXcS4Rrs93RwVkQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-euXcS4Rrs93RwVkQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-euXcS4Rrs93RwVkQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-euXcS4Rrs93RwVkQ .marker.cross{stroke:#333333;}#mermaid-svg-euXcS4Rrs93RwVkQ svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-euXcS4Rrs93RwVkQ p{margin:0;}#mermaid-svg-euXcS4Rrs93RwVkQ .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-euXcS4Rrs93RwVkQ text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-euXcS4Rrs93RwVkQ .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-euXcS4Rrs93RwVkQ .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-euXcS4Rrs93RwVkQ .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-euXcS4Rrs93RwVkQ .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-euXcS4Rrs93RwVkQ #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-euXcS4Rrs93RwVkQ .sequenceNumber{fill:white;}#mermaid-svg-euXcS4Rrs93RwVkQ #sequencenumber{fill:#333;}#mermaid-svg-euXcS4Rrs93RwVkQ #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-euXcS4Rrs93RwVkQ .messageText{fill:#333;stroke:none;}#mermaid-svg-euXcS4Rrs93RwVkQ .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-euXcS4Rrs93RwVkQ .labelText,#mermaid-svg-euXcS4Rrs93RwVkQ .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-euXcS4Rrs93RwVkQ .loopText,#mermaid-svg-euXcS4Rrs93RwVkQ .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-euXcS4Rrs93RwVkQ .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-euXcS4Rrs93RwVkQ .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-euXcS4Rrs93RwVkQ .noteText,#mermaid-svg-euXcS4Rrs93RwVkQ .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-euXcS4Rrs93RwVkQ .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-euXcS4Rrs93RwVkQ .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-euXcS4Rrs93RwVkQ .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-euXcS4Rrs93RwVkQ .actorPopupMenu{position:absolute;}#mermaid-svg-euXcS4Rrs93RwVkQ .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-euXcS4Rrs93RwVkQ .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-euXcS4Rrs93RwVkQ .actor-man circle,#mermaid-svg-euXcS4Rrs93RwVkQ line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-euXcS4Rrs93RwVkQ :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}在内存中极其快速地构建新的 VNode 树执行 O(n) 比对,找出所有差异 (找出 10 处修改)统一应用补丁:仅触发 1 次重排/重绘触发多次密集的状态更新新旧 VNode 树交接将差异打包为 1 个补丁 (Patch)
如上图所示,假设在极短的时间内发生了 10 次 DOM 修改操作。
- 原生 JS 的做法: 过桥 10 次,修改真实 DOM 10 次,触发浏览器重排 10 次。
- 虚拟 DOM 的做法: 在轻量级的 JS 内存中进行 10 次模拟修改(极快,无重排) -> Diff 算法比对出最终差异 -> 将这 10 个修改打包成一个补丁(Patch) -> 只过桥 1 次,统一操作真实 DOM,仅触发 1 次重排。
五、 结语
综上所述,原生 DOM 操作之所以笨重,是因为它背负了巨大的对象内存、跨界通信损耗以及极易触发重排重绘的机制。
现代前端框架并非拥有绕开浏览器操作 DOM 的魔法,而是通过虚拟 DOM 作为缓冲层,结合 Diff 算法作为差异计算器,最终实现了DOM 操作的批处理(Batching)。这种“在内存中计算最优解,再一次性提交给浏览器”的设计哲学,才是其保持高效渲染的根本所在。
