
Vue 3 和 React 谁性能更好,这是一个很容易吵起来的问题。可惜,这个问题大多数时候问错了。真实项目里,性能瓶颈很少只由框架名字决定。列表渲染规模、组件拆分、状态位置、请求策略、图片资源、打包体积、缓存策略、第三方库,都可能比框架本身更影响用户感受。
更实际的比较方式是:Vue 3 和 React 分别把性能优化放在哪些层面?默认情况下谁帮你做得更多?项目变复杂后,开发者需要掌握哪些优化手段?哪些问题是框架机制带来的,哪些只是代码写法问题?
Vue 3 和 React 都足够快,但它们的优化路径不同。Vue 3 倾向于结合响应式系统和模板编译做更细粒度的更新提示。React 倾向于通过重新执行组件得到下一份 UI,再通过协调过程和开发者控制边界来优化。
Vue 3 的性能基础有两层:响应式系统和模板编译。
响应式系统负责知道哪些状态被读取,状态变化后触发相关更新。模板编译器则把模板编译成渲染函数,并在编译阶段标记静态内容和动态部分。Vue 官方渲染机制文档提到,Vue 会使用静态提升、patch flag、树结构打平等方式减少运行时需要处理的内容。
用更通俗的话说,Vue 模板不是运行时才完全摸黑分析。因为模板语法受控,编译器能提前看出哪些地方永远不变,哪些地方只有文本变,哪些地方 class 或 props 变。运行时更新时,Vue 不必对所有节点都做同样成本的检查。
比如一个模板里有大段静态结构:
<template>
<article>
<header>
<h1>用户详情</h1>
<p>这里是固定说明文案</p>
</header>
<section>{{ user.name }}</section>
</article>
</template>Vue 可以识别静态部分,把更新重点放在动态文本上。开发者不需要手动给这段静态 header 做缓存。
这就是 Vue 默认体验的优势。业务开发者按常规模板写,框架和编译器会自动做一部分优化。不是说你完全不用关心性能,而是很多普通页面不需要上来就考虑 memo。
React 函数组件更新时,会重新执行组件函数,得到新的 JSX。React 再对比新旧结果,并把必要变化提交到 DOM。
这套模型非常简单:UI 是状态的函数。状态变了,函数重新执行,React 计算下一份 UI。简单带来可预测性,也带来一个常见问题:父组件更新时,子组件函数通常也会跟着重新执行,除非你做了边界控制。
例如:
function Parent() {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<ExpensiveChild />
</>
)
}count 更新时,Parent 重新执行,ExpensiveChild 也会被重新渲染计算。最终真实 DOM 不一定大量变化,但组件函数执行本身可能有成本。如果 ExpensiveChild 很重,就需要考虑 memo、组件拆分、状态下沉、children 组合等优化方式。
React 的优势是模型统一,优化边界清晰。你可以用 React.memo 缓存组件结果,用 useMemo 缓存计算结果,用 useCallback 缓存函数引用。但这些工具也有成本。滥用 memo 不一定提升性能,反而让代码更难读。
有些人会说 Vue 是细粒度响应式,所以性能不用管。这个说法不准确。
Vue 的响应式确实能减少一些无关更新,但组件仍然会有渲染成本。一个大组件里读取了很多响应式状态,只要相关依赖变化,组件渲染函数仍然要执行。大量列表、复杂计算、深层响应式对象、频繁 watch、副作用里同步重活,都会造成性能问题。
Vue 项目里常见优化包括:
computed 缓存派生值,而不是在模板里反复执行重计算。v-memo 或 v-once 优化特定稳定区域。shallowRef、shallowReactive 或 markRaw。Vue 提供了不少优化工具,但前提是你知道瓶颈在哪里。没有测量就盲目优化,往往只是增加复杂度。
React 项目里,memo、useMemo、useCallback 经常被误用。
React.memo 可以在 props 没变化时跳过组件重新渲染。但如果你每次都传新的对象或函数,它就很难发挥作用:
<Child options={{ size: 'small' }} onClick={() => save()} />这里 options 和 onClick 每次渲染都是新引用,即使内容一样,浅比较也会认为变了。于是很多人开始加 useMemo 和 useCallback:
const options = useMemo(() => ({ size: 'small' }), [])
const handleClick = useCallback(() => save(), [save])这在某些场景有效,但不是所有地方都值得做。缓存本身也有开销,还会增加依赖数组维护成本。如果组件很轻,重新渲染并不贵,memo 反而让代码变复杂。
React 的优化重点不应该是“看到函数就 useCallback”。更好的策略是先让状态位置合理。某个状态只影响局部,就不要放到很高的父组件里。某个昂贵子树不依赖变化状态,可以通过组件拆分、children 传入或 memo 控制边界。只有确认渲染成本明显时,再做精确缓存。
Vue 和 React 都需要稳定 key。很多性能和状态错乱问题,最后都能追到 key 上。
Vue:
<li v-for="user in users" :key="user.id">
{{ user.name }}
</li>React:
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}不要随便用数组下标当 key,尤其是列表会排序、插入、删除时。下标 key 可能导致组件状态错位,也会影响 diff 质量。稳定业务 ID 是更好的选择。
大列表问题也不是框架 diff 能完全解决的。真实 DOM 数量太多,浏览器布局、绘制、内存都会变重。无论 Vue 还是 React,几千行复杂列表都应该考虑虚拟滚动、分页、懒加载、窗口化渲染。
Vue 的 computed 是响应式系统里的缓存派生值。它会根据依赖自动失效:
const visibleUsers = computed(() => {
return users.value.filter(user => user.visible)
})只要 users.value 没有变化,visibleUsers 不会重复计算。模板读取它时也能拿到缓存结果。
React 的 useMemo 是在组件渲染过程中缓存某个计算结果:
const visibleUsers = useMemo(() => {
return users.filter(user => user.visible)
}, [users])它依赖开发者维护依赖数组。users 引用不变时返回缓存,引用变化时重新计算。
两者都能缓存计算,但 Vue 的 computed 更像响应式派生状态,React 的 useMemo 更像渲染期间的性能提示。React 官方也提醒过,不应该把 useMemo 当作语义保证;代码不应该依赖它才能正确,只应该把它作为优化手段。
很多用户感知到的慢,不是点击按钮后框架更新慢,而是首屏慢、资源大、接口慢、图片未优化、第三方库太重。
Vue 和 React 都可以配合 Vite、Rollup、webpack、ESBuild、SWC 等工具优化打包。也都支持代码分割、懒加载、路由级拆包。具体项目中,首屏性能更多取决于你是否合理拆包、是否延迟加载非关键模块、是否压缩图片、是否减少阻塞脚本、是否处理缓存策略。
如果应用用了 SSR 或 SSG,还要考虑服务端渲染成本、hydration 成本、流式渲染、边缘缓存等问题。React 生态里的 Next.js、Remix,Vue 生态里的 Nuxt 都在解决这些更高层的问题。单独比较 Vue 和 React 核心库的更新性能,并不能代表完整应用体验。
Vue 项目里,先相信默认优化,不要过早上复杂技巧。模板里避免重计算,派生数据用 computed。列表 key 稳定,大列表用虚拟滚动。大型不可变数据或第三方实例不要盲目 deep reactive。性能问题出现后,用 Vue Devtools 和浏览器 Performance 面板定位,再决定是否使用 v-memo、shallowRef、组件拆分等手段。
React 项目里,先把状态放对位置。不要把局部状态提到全局,也不要让一个父组件掌控所有变化。昂贵组件再考虑 memo,昂贵计算再考虑 useMemo,需要稳定传给 memo 子组件的函数再考虑 useCallback。不要为了“看起来专业”全项目铺满缓存。React 性能优化的第一原则是减少不必要的更新范围,而不是机械加 API。
Vue 3 的性能路线是响应式追踪加模板编译优化,默认情况下对业务页面比较友好。React 的性能路线是状态更新后重新计算 UI,再通过组件边界和缓存工具控制成本。Vue 不是不用优化,React 也不是性能差;它们只是把优化责任分配得不同。
如果团队希望框架和编译器默认兜住更多常规场景,Vue 会显得更省心。如果团队擅长组件边界设计,能够合理使用 memo 和状态拆分,React 可以支撑非常复杂的 UI。
真正专业的性能判断,不是问谁更快,而是先测量,再定位,再选择最小代价的优化方式。
Vue 里如果一段内容只需要渲染一次,可以使用 v-once:
<template>
<header v-once>
<h1>系统管理后台</h1>
<p>这段静态说明不会参与后续更新</p>
</header>
</template>如果列表项很多,但只有特定字段变化才需要更新,可以谨慎使用 v-memo:
<template>
<UserRow
v-for="user in users"
:key="user.id"
v-memo="[user.id, user.updatedAt]"
:user="user"
/>
</template>React 中,memo 适合包住确实昂贵、且 props 稳定的组件:
const UserRow = memo(function UserRow({ user, onSelect }) {
return (
<li onClick={() => onSelect(user.id)}>
{user.name}
</li>
)
})但如果父组件每次都创建新函数,memo 的效果会被削弱:
function UserList({ users }) {
const handleSelect = useCallback(id => {
console.log('select', id)
}, [])
return users.map(user => (
<UserRow key={user.id} user={user} onSelect={handleSelect} />
))
}更重要的是先减少更新范围。例如把输入框状态放在搜索区域内部,而不是放到整个页面顶层:
function Page() {
return (
<>
<SearchBox />
<ExpensiveDashboard />
</>
)
}
function SearchBox() {
const [keyword, setKeyword] = useState('')
return <input value={keyword} onChange={e => setKeyword(e.target.value)} />
}这类拆分通常比盲目 memo 更有效。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。