去年接手一个后台管理系统,代码是React 17写的,150多个路由,状态管理用了Redux。每次改一个需求,都要花半天去追状态流。React官方说组件是纯函数,单向数据流,可实际上当你对context高频写入时,下面所有组件都会跟着抖。我为了优化一个表格,把每一个单元格都包了memo,还是卡。后来发现是全局context里有一个timer在每秒更新。换成useSyncExternalStore之后好了,但那种“为了性能而歪曲设计”的感觉,特别别扭。
后来我偷偷在一个新模块用Vue 3重写,因为项目是webpack,兼容性没问题。Vue的响应式确实是另一种体验,不需要写memo,数据一变,用到的地方自动更新。初体验是真的爽。但爽了两个月后,问题来了。我在一个组件里写了一个watchEffect,里面访问了三个props和两个全局store,结果某天后端在轮询一个无关紧要的接口,那个接口的loading状态恰好进了全局store,我的watchEffect居然被触发了。排查了一下午,最后是打断点一个一个看依赖。Vue的自动依赖追踪就像个黑洞,表面上省心,实际上你根本不知道一个computed被哪些依赖“绑架”了。
然后我又试了Svelte 5,因为它的runes($state、$derived)把响应式变成了显式声明。这等于说:告诉编译器你要什么,而不是让运行时去猜。Svelte没有虚拟DOM,编译时生成操作原生DOM的代码,构建出来的bundle也小。但我用它写一个需要和第三方canvas库互动的组件时,发现$derived没法处理异步的依赖链,最后只能回到“手动赋值给普通变量”的老路子。Svelte把问题从“运行时不知道谁变了”变成“编译器知道谁变了”,但当你需要动态依赖源时,它又开始限制你。
所以你看,三者本质不是在比性能,而是在比“状态依赖可见度”。React用useMemo、memo全凭自觉,状态依赖藏在钩子闭包里;Vue依赖追踪自动建立,但状态依赖图是隐式的;Svelte用runes强制你声明依赖,却牺牲了某些动态性。实际上,前端领域正在朝着“信号”(Signals)收敛,Vue的ref、Svelte的$state、React的useSyncExternalStore本质都是信号,但每个人都在用自己的语法“遮挡”信号。我认为真正要解决的,不是用哪个框架,而是如何让复杂的依赖关系变得可观测。
所以我现在选框架的标准变了。我不看benchmark,不看star,只看一点:这个框架能不能让我在三个小时后还能说出“这个状态到底被谁引用了”。如果它提供了依赖图的可视化工具,那就是好框架。React DevTools的Hooks追踪太浅,Vue DevTools的依赖图仅限单个组件,Svelte的编译分析表又太底层。我甚至觉得,下一个叫“框架”的东西,不该再是UI渲染库,而应该是一个“状态拓扑图渲染器”。组件只是视图节点,而真正的架构,是那张你肉眼看不见的依赖网络。谁先把这张网画出来,谁就终结这场框架争论。