React 和 Vue 怎么选?先别翻性能对比表,换个维度看

🔑 关键词:前端框架选型,React,Vue对比,Svelte,hydration

📖 摘要:一篇不聊 benchmark 的前端框架选型文章。从真实业务里的 longtask 数据、hydration 机制差异,到「改一个需求要开几个文件」,聊聊 React、Vue、Svelte、Angular 在 2024-2025 年的真实取舍。

每次群里有人问 React 和 Vue 选哪个,五分钟之内一定会有人甩出 js-framework-benchmark 那张表,然后大家开始争谁比谁快 1.3 倍。我从 2019 年到现在做过四个从零开始的项目,两个 React、一个 Vue、一个 SvelteKit 写的内部工具,说实话我一次都没在真实业务里感受到过那 1.3 倍。

图片

去年给一个后台系统加性能监控,用 PerformanceObserver 采了一周的 longtask,P75 是 3 个,最长的那个 380ms。我一开始以为是表格组件的问题,点进去看火焰图,是导出 Excel 时 SheetJS 在主线程上解析那几万行数据。框架在这件事里连配角都排不上。

所以这篇不聊渲染速度。我想聊一个我认为更接近真相的维度。

真正让页面卡住的,是 hydration 不是 diff

diff 算法那点开销,在真实页面里基本可以忽略。有个更贵的东西:hydration。

React 18 的 hydrateRoot 是同步递归整棵 Fiber 树的。我拿一个 DOM 节点大约 4000 个的中等复杂页面测过,中端安卓机(骁龙 6 系那种)上,从 HTML 解析完到页面能响应点击,中间有 300 到 800ms 的时间窗口。这期间用户看着页面已经出来了,点了没反应。

图片

Vue 3 在这块是有优势的,模板编译阶段会做静态提升,纯静态的节点被 hoist 成一个常量 vnode,patch 的时候直接跳过。但注意,这是 patch 跳过,不是 hydration 跳过。它的 hydration 本身还是全量的,只是遍历成本低一些。

Svelte 4 因为没有虚拟 DOM,编译期直接生成 document.createElement 这类命令式代码,hydration 是逐个节点重建。Svelte 5 换成 runes 之后,这块的编译产物变了,但思路没变。

要真正绕开 hydration,目前只有两条路:Qwik 的可恢复性(resumability),把事件监听挂在 document 上用 QRL 恢复;或者 Astro 那种 islands 架构,默认零 JS,只有标了 client:load 的组件才下水。我在一个内容站上用了 Astro,首页 JS 从 Next.js 版本的 180KB 降到 12KB,Lighthouse 分数从 68 到 99。

但 hydration 这个问题本质上是历史遗留的。2016 年之前没人讨论它,因为那时候页面 JS 就几十 KB,同步执行完用户都没感觉到。是这几年 bundle 越来越大才把它变成一个话题的。

换个维度:改一个需求要开几个文件

图片

我现在选型会先问一个问题:一个需求变更,从读代码到测试通过,中间要打开几个文件、跨几层抽象?我管这个叫改动半径。

举个真事。需求是「提交按钮在请求中不能重复点击」。

React 项目里我改了三个文件:Button.tsx(加 disabled prop)、useSubmit.ts(加 isSubmitting 状态和 loading 分支)、types.ts(补一个字段)。因为 loading 状态存在 hook 里,Button 本身不知道这件事,得靠调用方传。

Vue 项目里我改了同一份 SFC 的两个地方:template 里的 :disabled="loading",script 里一个 boolean。

这不是 React 的错。React 的显式性是设计出来的,状态在哪、谁改的、什么时候改,都写在你脸上。对五个人以上的团队,这种显式性在半年后是有价值的;对一个三人小队,它是纯开销。Vue 的隐式响应式帮你省了胶水代码,代价是你改一个 ref,可能有四个地方悄悄跟着重渲染,onUpdated 里还得自己抓。

图片

状态管理,坑的形状不一样但都是坑

Redux Toolkit 的一个中等 slice,createSlice 加上 selectors,我数过大概是 50 行。Pinia 的一个 store 大概是 30 行。但这不重要。

重要的是心智模型。React 官方文档里有一整节叫「You Might Not Need an Effect」,专门讲别用 useEffect 去同步 props 到 state。我踩过这个坑:父组件传下来的 userId 变了,我 useEffectsetUser(userId),然后列表重新渲染两次,并且出现了一帧的旧数据。修起来不难,但你知道出问题的原因是「我把派生状态当成了状态」这件事,比修本身更值钱。

Vue 这边类似的是 watch。我写过 watch(() => props.value, ...) 然后在回调里改一个 ref,那个 ref 又被另一个 watch 依赖,绕了两圈才发现在打转。

我的感受是:React 的坑在「你能看见它」,Vue 的坑在「它不吵你」。前者适合有人在 Code Review 里拦你,后者适合你自己心里有数。

图片

2024 到 2025 这一年,几个框架都在动

React 19 在 2024 年 12 月发的稳定版,重点是 Server Components 和 Actions。RSC 的思路是把组件跑到服务端,客户端只接收序列化后的 payload,本质上是把「数据边界」从 fetch 层挪到了组件层。这个变化挺大的,Next.js 15 里 fetch 默认不再缓存就是配套的 breaking change,我升级的时候挂了两个页面,因为有个接口依赖了旧的默认缓存行为。

Svelte 5 在同一年 10 月发了正式版,$state$derived$effect 这套 runes 把响应式从编译器的黑魔法变成了显式 API。代价是代码变长了,let count = 0 变成 let count = $state(0)。我一开始挺抗拒,后来想想,这是拿几行样板换可读性,值。

Vue 3.5 官方博客里写的是响应式系统重写后内存占用降低 56%,这是他们自己测的数字,我没复现过,但升级过程确实没出问题。Angular 从 17 开始推 signals,到 19 的时候 signal 已经是一等公民了,Zone.js 的依赖也在慢慢减轻。这个方向大家其实在收敛。

我现在的判断标准

图片

不推荐具体框架,给你三个我会实际用的检查项:

一,团队里最闲的那个人,能不能两天内加出来一个带表单和列表的新页面。别问最厉害的,问最闲的。

二,全局搜一个组件名,结果能不能控制在 20 个文件以内。超过 20,说明你的抽象层数已经超过团队的理解带宽了。

三,升级一个大版本,需要改的文件数除以项目总文件数,能不能小于 5%。这条能帮你避开那些「升级即重写」的框架。

我个人现在新项目默认上 SvelteKit,不是因为快,是因为我能在周末两天里把一个想法从零推到线上,而我用 Next.js 的时候,光想清楚 data fetching 的边界就得花一个下午。这不是技术结论,这是我的时间预算决定的。你的可能不一样。

🏷️ 标签: