
还在用虚拟DOM的兄弟们2026年这个风向真得看清了。我们这行每年都会冒出几个革命性概念但这次的动静不一样信号Signals开始以前端框架的标准能力大面积落地而曾经被奉为圭臬的虚拟DOM diff算法正被越来越多的新框架从核心渲染链路上请出去。从SolidJS、Svelte 5到Angular再到Vue 3.4的响应式重构几乎每一家都朝同一个方向掉头——如果你还在把虚拟DOM性能好当默认前提写进技术方案这篇文章值得花十分钟看完。这篇文章我会结合自己这几年的实操观察讲清楚信号到底是什么、它凭什么取代虚拟DOM的位置、以及2026年主流框架各自背叛虚拟DOM的真实姿势和代价。还会拆几个实际的代码场景聊聊迁移信号式架构时踩过的坑包括面试里关于虚拟DOM和diff算法的新老考法。无论你是在调研下一代框架选型还是单纯想搞懂信号这个刷屏名词都能从里面拿到能直接用的判断框架。1. 虚拟DOM的辉煌与隐秘代价它凭什么统治了十年又凭什么被嫌弃要理解信号为什么崛起得先回到虚拟DOM最风光的那几年弄清楚它解决的是什么问题又埋下了哪些后来被系统性讨伐的隐患。1.1 当年为什么选择虚拟DOM一个中间态的妥协前端渲染的原始困境是直接操作DOM太慢而状态一变就全量重绘更慢。浏览器把DOM实现成一棵庞大、带副作用的树每一次插入、删除、属性变更都可能触发样式重算和布局抖动。早期的jQuery时代靠人肉维护DOM操作应用复杂以后根本收不住。虚拟DOM的聪明之处是给真实DOM加了一个廉价的影子副本。状态变化时框架先在JS内存里构建一棵新的虚拟DOM树然后和上一棵旧树做diff对比找出最小差异集合最后才把这些差异批量提交给真实DOM。因为JS对象对比的成本远低于真实DOM操作的代价这个方案在很长一段时间里都是最优解——相当于你改稿子不会直接划掉印刷好的书页而是在草稿纸上先改好再把最终改动誊抄上去。React用它统一了声明式UI的心智模型你只管描述UIfn(state)框架负责计算和落地。这套思路后来被 Vue、Preact、Inferno 等大量框架吸收成了前端开发十年间的默认基础设施。1.2 2026年的diff困境一张逐年变贵的成本账单但虚拟DOM的最优解身份是有前提的组件数量不大、状态更新频率不高、UI结构相对静态。2026年的真实前端应用早就不满足这些前提了diff的成本结构正在肉眼可见地恶化。第一笔账是内存。虚拟DOM树是真实DOM树的完整JS镜像一个中型后台项目同时存在几千个组件实例、上万个虚拟节点是常态。每次更新都要重新创建一棵新树旧树再等着被垃圾回收。数据表格、在线文档、实时大盘这类高频更新场景里GC停顿和内存抖动是会真实影响帧率的。第二笔账是diff本身的计算量。diff算法对树进行递归遍历虽然做了key优化和层级跳过但复杂度仍然和组件树的规模强相关。一个看起来只改了一格的表格可能要遍历成百上千个虚拟节点才能定位到那个单元格。第三笔账最致命——重新执行组件函数。为了生成新的虚拟DOM树框架必须重新调用组件函数、重新执行所有JSX表达式。哪怕状态只影响一个按钮的文案整个组件子树都要重新跑一遍。这个不知道哪儿变了所以全量算一遍的策略在应用变复杂后成为性能瓶颈的主要来源。这不是说虚拟DOM被淘汰是因为它错了而是它把一个通用但略笨的方案推向了极致。当应用反复出现99%的渲染工作都在做无用功时人们自然开始寻找一种能精准知道到底谁变了的机制——这就给信号留出了历史身位。2. 信号是什么从找变化到知道变化的范式翻转信号并不是2026年才诞生的新东西它比虚拟DOM年纪还大。Knockout的observable、Ember的computed、Vue 1.x的依赖收集骨子里都是信号思想。只是当年的工程成熟度和生态支撑不起这套机制走向主流。如今框架基础设施够硬了信号才终于以新一代默认方案的形态杀回舞台中心。2.1 用一个生活类比理解信号机制水电改造vs全屋重刷把虚拟DOM方案想成全屋重刷模式家里有一盏灯坏了某处状态变了你不知道是哪一盏于是把整面墙的电路重新排查一遍diff整棵组件树再决定换哪个灯泡更新真实DOM。这套思路稳妥、通用、好理解但每次修一盏灯都要惊动整屋墙皮。信号方案是水电改造模式先给每一盏灯装好独立回路状态与视图之间建立精确依赖关系灯泡坏了电流直接沿着对应的那根线就流过去了不需要碰其它任何墙面。换句话说虚拟DOM是事后抓差异——先渲染、再对比、再定位信号则是事前建通路——哪个视图读了哪个状态框架在一开始就记录在案状态一变相关视图立刻精准更新。前者花时间找哪里变了后者把找这件事彻底取消了。2.2 信号三件套Signal、Computed、Effect信号体系最核心的API通常就是这三个不同框架叫法略有差异但思想高度统一Signal信号源一个携带可变值的容器。读取值时会被记录为依赖方写入新值时通知所有依赖方。对应代码里类似const count signal(0)这样的声明。Computed派生信号由其它信号计算得到的只读信号。它内部会缓存计算结果只有依赖的真实变化时才重新计算实现惰性求值。对应const doubled computed(() count() * 2)。Effect副作用当依赖的信号变化时自动执行的函数通常负责DOM更新、网络请求、日志等副作用操作。对应effect(() console.log(count()))。这里最反直觉的一点是依赖关系不需要你手动声明。框架利用的正是JavaScript的读取即捕获特性——当你执行count()时Signal内部就把当前正在运行的Effect或Computed记到自己的订阅列表里了。它知道我变了要通知谁这些谁是运行时自己收集的不是靠开发者在依赖数组里手填的。2.3 信号为什么比diff快三张成本表告诉你钱省在哪用一组具体数字来感受一下虚拟DOM和信号在更新链路里的差距。假设有一个1000行、每行50个单元格的数据表格现在要修改其中一行的一个数字。虚拟DOM的更新流水账环节耗时说明估算量级重新执行组件函数整个表格组件及其所有子组件重新执行一遍生成新的虚拟DOM树可能需要执行数千次函数调用构建新虚拟DOM树大量JS对象被创建数万级节点的内存分配diff旧树与新树递归遍历、比较属性与子节点树越大越慢定位差异节点最终找到那一个需要更新的单元格真正有价值的计算提交DOM更新只更新目标单元格一次直接操作信号的更新流水账环节耗时说明估算量级信号写入新值通知订阅者常数时间computed重算如果有派生值则重新计算常数时间effect挂载的DOM操作直接找到目标单元格的更新函数并执行一次直接操作注意信号方案里压根没有重新执行组件函数和diff这两个环节。省下来的不只是计算时间还有整棵虚拟DOM树的创建与回收内存成本。表格场景只是例子凡是高频交互、大数据量、复杂嵌套组件的应用信号的优势都会被放大。当然信号也有自己的成本每个信号、每个订阅关系都要占内存依赖追踪发生在运行时粒度太细可能造成大量小闭包。只是这些成本比整树diff要小得多属于省大钱花小钱的买卖。3. 2026年框架的叛逃全景谁在拥抱信号谁在妥协现在来逐个盘点主流框架的选择。你会发现背叛虚拟DOM不是某个框架的孤勇而是整个行业的集体转向只是每条路线各怀心思、方案取舍也不同。3.1 编译时派SolidJS与Svelte 5把信号焊进编译产物SolidJS是激进派的代表。它从诞生起就没有虚拟DOM组件函数只执行一次UI结构在初始化时被编译成一个个独立的细粒度更新表达式。每次状态变化渲染层直接调用对应的DOM更新语句。它的JSX写法看起来和React高度相似淘汰学习成本却低得很——底层机制完全不同。Svelte 5则在2024年到2026年间完成了最重要的架构升级用信号向的runes机制全面取代了原来的编译时脏检查方案。旧版Svelte靠编译器静态分析变量赋值来驱动更新一旦变量通过对象属性、解构、动态key等间接方式修改就会失效runes相当于在编译产物里显式插入了信号读写代码响应式的精确度和健壮性都上了一个台阶。编译时方案的好处是运行时“零成本”信号逻辑在构建阶段被摊平到原生操作里浏览器端不需要加载响应式系统的运行时库。代价是框架的编译器和工具链变得极其复杂模板写法也会被框架进一步锁死。3.2 激进派Qwik与Angular的信号化改造Qwik的卖点是可恢复性通过序列化应用状态和监听器让页面在服务端渲染后能做到几乎零JavaScript的交互恢复。它同样基于信号机制且信号天然契合它的粒度控制——每个信号可以独立恢复、独立更新不需要在初始化阶段把所有逻辑都执行一遍。Qwik这种按需加载更新路径的能力如果建立在虚拟DOM全量diff的底座上根本跑不起来。Angular从v16开始引入信号API并在后续版本中大力推动基于信号的新变更检测机制。老Angular的zone.js方案是猴子补丁全局事件然后从根组件全量检查一遍性能特征和虚拟DOM有点殊途同归的意思——都属于不知道谁变了所以全查一遍。信号化之后Angular终于可以跳过那些与状态无关的组件子树这对其大型企业级应用的用户体验是实打实的改善。不过Angular的路线是渐进兼容新老机制共存模板里的传统绑定和signal()绑定完全可以混用好处是平滑迁移坏处是心智负担变重。3.3 保守派Vue 3.4 与 React 19 compiler 的务实妥协Vue其实是老信号人了Vue 1.x的核心就是依赖收集后来为了对齐React生态才引入虚拟DOM。Vue 3的响应式系统Proxy effect本来就是信号式但渲染层仍是虚拟DOM做patch。3.4版本开始Vue在编译器里加入了响应式细粒度标记模板中哪些节点依赖了哪些响应式变量会在编译期被标注出来patch阶段可以直接跳过未依赖变更的节点。可以说Vue现在的架构是信号做数据流虚拟DOM做兜底渲染属于最务实的中间路线。React 19的动作最耐人寻味。React官方没有直接下场做信号而是推出了React Compiler编译器自动为组件做记忆化把那些每次渲染都要重新执行的地方尽量缓存起来。React团队的态度很明确——虚拟DOM的封装模型函数组件 自上而下的重新渲染是React的立身之本他们想在不摧毁心智模型的前提下缓解性能问题。但社区并不完全买账很多人在讨论编译器是否只是给旧引擎缝缝补补。甚至React生态里已经出现了让React直接运行在信号上的运行时方案老树发新芽的戏码还在上演。3.4 为什么偏偏是2026年三个推力共同引爆信号的思想出现得比虚拟DOM早为什么到2026年才形成集体叛逃的势头推力一应用复杂度让diff的浪费变得不可接受。实时协作、大数据可视化、AI流式输出逐token刷新UI这类场景把状态更新频率拉到了每秒几十次甚至上百次在这种频率下虚拟DOM的全量计算浪费是肉眼可见的卡顿。推力二编译器能力的成熟。信号要真正普及需要编译器把声明式写法高效地转译成细粒度更新代码。早期JavaScript工具链做不到这个层面的优化现在SWC、Vite、Rolldown这些基础设施强了框架才能在编译期大展拳脚。推力三框架竞争的鲶鱼效应。当SolidJS靠细粒度性能拿到大量口碑Svelte靠信号升级扭转了旧架构的先天缺陷Angular也靠信号化重新赢得关注还在坚持老路线的框架就面临巨大的社区压力。别人都在抛弃虚拟DOM这件事本身就会加速剩余框架跟进。4. 跑通一个最小信号应用两种写法的真实对比光讲原理不够还得上手写。我拿一个最常见的待办列表筛选场景把React式的虚拟DOM写法和SolidJS的信号写法摆在一起看看真实代码长什么样。4.1 从React思维迁移count时代的直球写法先看最基础的计数器。React写法是这样的function Counter() { const [count, setCount] useState(0); return ( button onClick{() setCount(count 1)} {count} /button ); }SolidJS的写法是这样的function Counter() { const [count, setCount] createSignal(0); return ( button onClick{() setCount(count() 1)} {count()} /button ); }表面看只差一个括号的区别但这正是信号和虚拟DOM的分水岭。Solid里点击按钮时只会执行按钮对应的更新表达式不会重新调用Counter函数本身。而React里setState会触发整个Counter组件重新执行再走一遍diff。组件大了差距就出来了React里一次点击可能让几百个节点跟着陪跑Solid里除了那个按钮全世界静止。4.2 列表与过滤没有diff的真实更新现场再看一个稍复杂的场景一个输入框实时过滤一份列表。React写法function FilterList({ items }) { const [keyword, setKeyword] useState(); const filtered useMemo( () items.filter(item item.includes(keyword)), [items, keyword] ); return ( div input value{keyword} onChange{e setKeyword(e.target.value)} / ul {filtered.map(item li key{item}{item}/li)} /ul /div ); }这里我们必须用useMemo手动控制过滤运算否则每次输入一个字符整个列表都会重新filter一遍。依赖数组写错缓存就失效这是React开发里最常见的性能翻车点之一。Solid写法function FilterList(props) { const [keyword, setKeyword] createSignal(); const filtered createMemo( () props.items.filter(item item.includes(keyword())) ); return ( div input value{keyword()} onInput{e setKeyword(e.target.value)} / ul For each{filtered()} {(item) li{item}/li} /For /ul /div ); }注意Solid里filtered是createMemo创建的派生信号。它缓存了过滤结果只有keyword()或props.items真实变化时才会重新计算。这个缓存行为是内置的不用你手动写依赖数组。输入框每敲一个字符Solid会直接定位到ul内部的更新逻辑对列表做最小增删不会重新渲染任何无关节点。这里有个关键实战心得别在Signal组件里随便用普通函数做派生数据。如果没有createMemo包装每次访问都会重新计算相当于把记忆化的收益全丢了。记住口诀普通数据用signal计算数据用memo副作用用effect。4.3 迁移期最容易被架构坑的四个点从虚拟DOM迁移到信号式框架代码语法是小事思维惯性才是大坑。我亲测踩过的和看同事踩过的基本集中在四个地方坑一把signal当普通值解构出来用。在React里state和setState是一对你可以在子组件里随便用state。但在信号式框架里如果你从一个store里解构出signal的当前值而不是signal本身就会丢失响应式。比如在Solid里const { count } store这个count是复制出来的死值后续store里count怎么变这个组件都不会再更新。正确做法是保留引用或者使用框架提供的store解构API如Solid的useStore解构仍然保有响应式代理。坑二把在组件顶层读取signal误以为和在局部读取signal一样。信号式框架同样有组件级作用域的概念。如果在组件函数顶层读取signal这个组件整体就变成了该信号的订阅者任何变化都会让组件重新执行。如果只在某个子表达式、某个effect里读取更新范围就被限制在那个局部。换句话说读取信号的位置决定了更新的边界。从虚拟DOM迁移过来的人总是习惯性地在一个函数里把所有状态都读一遍然后抱怨怎么还是哪里都更新其实这是自找的。坑三条件分支里的computed陷阱。computed是惰性的只有在被读取时才会求值并且只在那次求值过程中建立依赖。如果某个依赖字段不是每条执行路径都会访问它就不会被追踪。经典翻车现场const info computed(() condition ? a() : b())一开始condition为true后面a变了、b也变了但condition还是true那么b根本不在依赖表里——b怎么变都不会触发这个computed。这不算bug是设计行为但没经验的人会排查到崩溃。坑四跨组件通信的粒度把握。信号式框架里全局signal用起来太顺手很多人一上来就把所有状态都扔进全局store。短期看着爽长期就是灾难所有组件都可能订阅同一个信号更新范围被全局化粒度优势荡然无存。我的建议是沿用状态尽量靠近使用它的组件的原则全局信号只保留真正的全局态用户信息、主题配置、权限标记业务数据还是走props或局部store。5. 常见问题排查与面试实用手册信号式架构上手快但调试和团队协作里的坑往往比预期多。同时虚拟DOM和diff算法面试依然是前端面试的常青树只是现在考题正在从手写diff升级成信号和diff谁更优。这一节把这两条线都给你捋顺。5.1 信号应用调试的三个实战痛点痛点一调用栈不再有组件名。虚拟DOM模式里一次点击引发的函数调用是组件A → 组件B → 组件C这种层级清晰的结构出错时一眼能看出是从哪个组件冒出来的。信号模式里一次更新可能是任意一个effect里触发的DOM操作调用栈里往往只有getter/setter和更新函数很难直观地看出这是谁的更新。我的应对办法是给关键effect加有语义的日志前缀用console.debug([TodoList:effect], ...)这种方式强行标记来源排查效率能翻倍。痛点二DevTools的火焰图失效。React DevTools的Profiler记录了每个组件的渲染耗时大家习惯了靠这个定位性能瓶颈。信号式框架没有组件重新渲染的概念自然没有对应的火焰图。你只能借助框架自带的调试工具如Solid DevTools或者浏览器Performance面板从真实的布局/绘制/脚本时间入手反推。这要求开发者对浏览器渲染管线的理解更深对新手来说反而是一个隐性门槛。痛点三和既有状态库的摩擦。如果项目里有大量基于Redux/Zustand等外部store的状态逻辑迁到信号式框架时要么把它们封装成框架的信号要么接受部分状态走信号、部分状态走store的双轨制。封装时要注意避免双重订阅——store自己派发更新、信号又代理了一遍容易触发重复渲染和诡异时序。我建议在迁移初期把外部store当作数据源输入把框架信号当作视图状态输出中间用effect做单向同步别让两条响应式系统互相直接穿透。5.2 信号diff混合架构不是非黑即白别把虚拟DOM和信号看成有你没我的关系。2026年的现实是混合架构大量存在。Vue 3已经是典型的混合体响应式数据是信号化渲染patch是虚拟DOM靠编译器的细粒度标记在patch时跳过无关节点。这种方案的价值在于既保留了模板语法和生态兼容性又获得了大部分信号性能红利。Angular也类似信号驱动更新但变更检测机制仍然保留了完整的组件树检查作为兜底。所以做技术选型时不必执着于我要纯信号框架更务实的判断标准是你的性能瓶颈是在创建大对象树、走diff、重新执行组件函数这三个环节吗如果主要在信号化收益巨大如果当前项目只是普通CRUD页面虚拟DOM的浪费根本感知不到迁移反而要付出学习和重构成本。5.3 面试官问为什么信号比虚拟DOM快该怎么答这道题已经取代部分手写diff成为前端面试的新宠。注意面试官通常期待的不是一句信号不用diff而是完整的对比分析。我建议按这个框架答先承认虚拟DOM的历史合理性它解决了声明式UI如何高效更新的问题通过JS内存树对比减少真实DOM操作。再指出diff的成本结构重新执行组件函数是主要浪费树越大、更新频率越高、浪费越明显。最后讲信号的机制依赖追踪 细粒度订阅状态变化直接驱动精确的更新函数取消了整树diff和组件级重执行。补充边界条件信号的内存开销不小依赖追踪在复杂嵌套场景下也有性能损耗虚拟DOM在中小规模应用里简单直接反而更好维护。如果被追问为什么现在才流行给出编译器成熟、应用复杂度、生态竞争三个推力。这个答法既展示了底层理解又展现了工程权衡意识比单纯背概念得分高得多。5.4 什么样的项目应该留在虚拟DOM阵营坦白讲并不是所有项目都该背叛虚拟DOM。我自己的判断标准很简单如果你维护的是大型成熟React/Vue应用迁移到信号式框架的代价极高收益却可能感受不到。不如拥抱React Compiler、Vue的编译时优化用最小改动吃一部分性能红利。如果你的应用是重表单、重文档流、强SEO的偏静态页面虚拟DOM的整树执行并不算什么瓶颈信号化带来的收益也不明显。如果你在做一个从零起步的新项目且预判未来会有高频交互、大型列表、复杂派生数据那可以直接考虑SolidJS、Svelte这类纯信号框架。省下未来做性能优化的心思真的挺香。我最近的实际体验是把一个内部报表工具的数据面板从类React写法改成Solid之后最直观的变化不是跑分而是输入筛选关键词时页面从偶尔卡顿变成了丝般顺滑。卡顿不是凭空消失的是被信号精确到cell级更新的机制按下去的了。6. 最后分享两个实操层面的心得第一个心得是信号的性能优势要用在刀刃上别滥用。我见过有同事把所有数据都做成全局信号导致任何一处的微小变化都会触发大量effect性能比虚拟DOM还差。信号是一种精确的工具但它要求你精确地知道哪个视图依赖哪个状态。我的习惯是先用普通props和局部状态搭好结构真的遇到性能瓶颈再引入信号优化热点路径而不是一上来就全局信号满天飞。就像装修时先做好水电总闸然后只在关键房间装独立回路而不是给所有插座都单独拉一根线。第二个心得是关于团队协作的迁移到信号式框架最大的挑战不是技术而是review习惯的重建。代码review时过去大家会盯着依赖数组写得对不对、useCallback包没包、memo加没加现在这些都不存在了新的关注点是这个signal在哪里被读取computed依赖了哪些字段effect是否有不必要的订阅。建议团队在迁框架前先统一一份关于信号使用规范的清单把读取位置决定更新范围computed里别写副作用全局信号必须走审批这类约定固定下来不然代码库很快会变成一盘所有人都在互相影响的意大利面。说句实在话写了这么多年前端我已经不太相信有什么一劳永逸的架构了。虚拟DOM统治了十年解决了很多问题也积累了新的瓶颈。信号本质上是一次回归正确轨道——把应该精确更新哪些东西这件本来就能做到的事重新做对了而已。2026年这个节点与其纠结哪个框架站队漂亮不如先把信号这套新心智模型装进脑子因为它大概率会在你未来的项目里遇到不分框架。