news 2026/8/23 23:41:24

改一个叶子要重渲染 1 万次,Signals 只跑 1 次:细粒度响应式的实测复盘

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
改一个叶子要重渲染 1 万次,Signals 只跑 1 次:细粒度响应式的实测复盘

2026 年,Angular、Vue、Solid、Svelte 几乎同时把「细粒度响应式 / Signals」推到台前,社区共识是它比虚拟 DOM 的整树重渲染更快。但「更快」到底快在哪、快多少、有没有失效场景,大多停留在口号。我用约 50 行 JS 在 Node 22 搭了两套最小系统,做了一次可复现的对照。

背景:为什么这件事值得写

虚拟 DOM 的心智模型是「状态变 → 重渲染整棵组件树 → diff → 打补丁」。即便只改了一个按钮文案,React(未加 memo)默认也会重跑整棵树的组件函数。Signals 的做法相反:在值层面精确记录「谁读了它」,状态变化时只更新真正依赖它的那一个 DOM 节点,不做 diff、不重跑父组件。

共识归共识,工程上仍有三个没说清的点:第一,所谓「更快」省下的到底是什么——是 diff 成本,还是组件函数本身的重执行?第二,省下的量到底和什么成正比?第三,有没有信号救不了的场景?不量化,这些就只是信仰。

解剖:两套最小系统怎么搭

为了把变量控制干净,我没有引入任何框架,而是各写了约 25 行的最小实现,保证两者「做等量真实计算」后再比时间。

系统 A(整树重渲染)持有一个长度为 N 的 state 数组,每个组件函数读取自己那份 state。update时无条件把所有组件函数重新跑一遍:

// 系统 A:任一状态变化 → 遍历重跑全部 N 个组件函数 function makeTreeModel(N) { const state = new Array(N).fill(0); const renders = new Array(N); for (let i = 0; i < N; i++) renders[i] = () => state[i] * 2654435761 >>> 0; return { update(idx, v) { state[idx] = v; for (let i = 0; i < N; i++) renders[i](); } }; }

系统 B 是一个最小 signal:get()在执行 effect 期间自动把当前 effect 登记为订阅者,set()只通知这些订阅者,绝不波及无关组件:

// 系统 B:getter 自动收集依赖,setter 只推送给订阅者 let activeEffect = null; function signal(value) { let v = value; const subs = new Set(); return { get() { if (activeEffect) subs.add(activeEffect); return v; }, // 读时收集依赖 set(nv) { v = nv; for (const e of [...subs]) e.run(); }, // 写时只推送订阅者 }; } function effect(fn) { const e = { run() { const prev = activeEffect; activeEffect = e; fn(); activeEffect = prev; } }; e.run(); }

两者的关键差别不在「算什么」,而在「哪些组件函数会被重新执行」。下面用 1 万组件把这件事压出来。

图1:左为整树重渲染——任一状态变化都重新执行全部组件函数;右为细粒度信号——getter 自动收集依赖,只重跑读过它的那一个组件。

实证:1 万组件的对照结果

实验环境为 Node 22.22.2,纯 JS 无第三方依赖。构建 N 个组件,重复更新 K=2000 次后统计「组件函数重执行总次数」与耗时。为保证公平,两套系统的每次重执行都做相同的整数乘法并累加到同一个 sink,确认两者做了等量真实计算(局部场景 sink 完全相等)。

先测「局部更新」——只改下标为 0 的那一个叶子:

场景组件数 N每次更新重执行2000 次累计耗时(ms)
局部 · 整树重渲染2,0002,0004,000,00026.9
局部 · 细粒度信号2,00012,000≈0.1
局部 · 整树重渲染5,0005,00010,000,00077.6
局部 · 细粒度信号5,00012,000≈0.1
局部 · 整树重渲染10,00010,00020,000,000178.3
局部 · 细粒度信号10,00012,000≈0.1

结论很直接:局部更新时,整树模型每次都要重跑「全部 N 个」组件函数,细粒度信号每次只重跑「那 1 个」。组件数从 2 千涨到 1 万,前者的耗时从 26.9ms 涨到 178.3ms,近乎线性;后者始终 ≈0.1ms,几乎恒定。两者耗时差约 1,700 倍,重执行次数差正好是 1 万倍。

图2:横轴为组件数,橙色为整树重渲染耗时(随 N 近乎线性增长),绿色为细粒度信号耗时(始终低于 1ms)。

复现命令(文章同目录bench.mjs,纯 JS 无依赖):

# Node 22.x node bench.mjs

局限:信号不是万能

把「局部更新」换成「广播更新」——改一个被全部组件读取的共享值,结果立刻反转:

场景每次更新重执行2000 次累计说明
广播 · 整树重渲染10,00020,000,000全部重跑
广播 · 细粒度信号10,00020,000,000全部订阅者都被通知

当依赖被所有组件共享时,细粒度信号同样要重跑全部 1 万个订阅者——推送模型并没有减少「该跑的组件」数量,只是省掉了 diff。也就是说,信号的优势严格绑定在「变更是局部化的」这一前提上。除此之外还有几个常被忽略的边界:订阅图本身要占用内存并随组件数维护;SSR 阶段信号需要额外的运行时追踪开销;依赖关系不可见会让调试更费劲;而对十几个状态的小型应用,三者差异用户根本无感。

图3:局部化变更时信号省下 1 万倍重执行;共享广播时两者完全相同,信号并无优势。

结论与下一步

一句话方法论:细粒度响应式省下的是「与组件规模成正比、但与变更粒度无关」的重渲染,所以把它当作热路径优化(大表单、实时仪表盘、万行列表)才划算,而不是当作全局架构信仰。广播型状态、小型应用、SSR 首屏,该用整树模型或编译期 memo 仍要用。

开源地址

  • 矩阵门户:https://github.com/wangzifan396-wzf/WB
  • 单文件工具聚合器:https://github.com/wangzifan396-wzf/nano-workbench
  • GitHub 组织主页:https://github.com/wangzifan396-wzf
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/23 23:39:59

大学生用 AI 学编程的正确姿势:从问答案到做验证

大学生用 AI 学编程的正确姿势&#xff1a;从问答案到做验证 AI 可以帮助大学生理解概念、生成示例、定位错误&#xff0c;但“得到一段能运行的代码”不等于“学会了编程”。真正可靠的学习闭环应当是&#xff1a; 提出问题 → 获取假设 → 动手实现 → 设计验证 → 分析结果…

作者头像 李华
网站建设 2026/8/23 23:39:59

2026年5款AI写网文剧本工具实测横评:谁才是终极消痕助手?

搞了整整一周&#xff0c;把市面上吹得天花乱坠的AI写作工具全部测了一遍。 说实话&#xff0c;作为写了十年网文、敲过无数剧本的重度患者&#xff0c;我最近被各种“降AIGC率”、“智能润色”折腾得够呛。现在不管是网文编辑还是剧本甲方&#xff0c;只要用检测工具一扫&…

作者头像 李华
网站建设 2026/8/23 23:38:08

真理不需要验证:KTS理论对西方学术范式动机污染的彻底诊断

真理不需要验证&#xff1a;KTS理论对西方学术范式动机污染的彻底诊断摘要本文以 112 作为 T0 级绝对真理标杆&#xff0c;完整梳理贾子理论&#xff08;KTS&#xff09;的认识论内核&#xff0c;划清真理本身、陈述行为、猜想、方法四者边界。阐明纯粹性检验标准、TMM 三层架构…

作者头像 李华
网站建设 2026/8/23 23:37:34

探秘 Python 枚举类型:从基础到实战的深度指南

探秘 枚举类型&#xff1a;从基础到实战的深度指南深入介绍本文涵盖Enum、Flag等多种枚举类型的枚举种类, 从基本概念开始介绍, 再介绍创建方式, 接着介绍成员访问, 然后介绍比较运算等方面作出全面详细解析, 结合丰富示例与直观图表, 助力读者全面掌握枚举知识, 提升代码的可读…

作者头像 李华