渲染性能优化手段深度解析
从 React 的 React.memo/useMemo/useCallback 到 Vue 的 v-memo/计算属性,详解框架级渲染优化的原理、陷阱和正确用法
一句话概括
框架做了「数据变了就重新渲染」,但框架改不了「不该重新渲染时也重新渲染」——渲染优化的核心就是教框架跳过不值得的渲染,React 用 memo 体系(React.memo + useMemo + useCallback),Vue 天生就帮你做了大部分。
核心知识点
1. React.memo:跳过 props 不变的子组件
1
2
3
4
5
6
7
8
9
10
11
// ❌ 没有 React.memo:父组件重渲染 → 子组件必定重渲染
function Child({ name }: { name: string }) {
console.log('Child render') // 每次父组件更新都打印
return <div>{name}</div>
}
// ✅ 加了 React.memo:对 props 做浅比较 → 不变就跳过
const Child = React.memo(function Child({ name }: { name: string }) {
console.log('Child render') // 只有 name 变化时才打印
return <div>{name}</div>
})
但这有个坑——引用类型的 props 每次都是新对象:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Parent() {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(c => c + 1)}>+1 ({count})</button>
{/* 🔴 handleClick 每次渲染都是新函数 → React.memo 失效 */}
<Child name="Alice" onClick={() => console.log('clicked')} />
{/* 🔴 data 每次渲染都是新对象 → React.memo 失效 */}
<Child name="Bob" data={{ id: 1 }} />
</>
)
}
2. useMemo + useCallback:稳定对象和函数的引用
React.memo 做的是浅比较,所以对象和函数每次渲染都「不一样」——useMemo 和 useCallback 就是来稳定这些引用的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Parent() {
const [count, setCount] = useState(0)
// ✅ 函数引用稳定:count 不变时 handleClick 是同一个引用
const handleClick = useCallback(() => {
console.log('clicked')
}, []) // 空依赖 → 永远稳定
// ✅ 对象引用稳定
const data = useMemo(() => ({ id: 1 }), [])
return (
<>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<Child name="Alice" onClick={handleClick} data={data} />
</>
)
}
黄金法则:React.memo + useMemo + useCallback 必须三件套搭配使用,缺一个就全白费。
3. 值比较 vs 引用比较:自定义比较函数
1
2
3
4
5
6
7
8
9
// 默认浅比较:只比第一层
const Child = React.memo(Component) // 等同于 React.memo(Component, shallowEqual)
// 自定义比较:精确控制「什么时候算变了」
const Child = React.memo(Component, (prevProps, nextProps) => {
// 返回 true → 跳过渲染(props 没有变化)
// 返回 false → 需要重新渲染
return prevProps.user.id === nextProps.user.id
})
注意返回值是反直觉的:React.memo 的比较函数返回 true 表示「不变(跳过渲染)」,返回 false 表示「变了(需要重新渲染)」,和 shouldComponentUpdate 语义一致。
4. Vue 的自动优化:不需要 memo
这是 Vue 在开发体验上最大优势之一:
1
2
3
4
5
6
7
8
9
10
11
<!-- Vue:默认就不重新渲染不变的子组件 -->
<template>
<Child :name="'Alice'" :onClick="handleClick" :data="{ id: 1 }" />
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
// handleClick 在 setup 中定义 → 只创建一次,引用天然稳定
const handleClick = () => console.log('clicked')
</script>
Vue 不需要 useCallback 的根因:<script setup> 中的代码只执行一次,函数引用天生稳定。React Hooks 中函数每次渲染都重新执行——这是两套响应式模型对开发体验最直接的影响。
Vue 的 v-memo 指令则是针对模板内部的精细控制:
1
2
3
4
<!-- 只有 item.id 或 selected 变时才重渲染这个 div -->
<div v-for="item in items" :key="item.id" v-memo="[item.id, selected]">
{{ item.name }}
</div>
5. shouldComponentUpdate(类组件时代)
1
2
3
4
5
6
7
8
9
class Child extends React.Component {
// 手动控制是否重渲染——这是 React.memo 的类组件版本
shouldComponentUpdate(nextProps, nextState) {
return this.props.name !== nextProps.name
}
render() {
return <div>{this.props.name}</div>
}
}
现代 React 已经完全转向函数组件,但面试中 shouldComponentUpdate 仍然会出现——理解它有助于理解 React.memo 的返回值为什么是反直觉的。
其实你每天都在用
React.memo包裹列表项:items.map(item => <MemoItem key={item.id} data={item} />)——不做这个优化,父组件状态一变整个列表重渲染,即使列表数据完全没变。useCallback稳定回调:传递给useEffect的函数如果不用useCallback,依赖数组就永远不全、ESLint 报 warning——这不是 bug 而是 feature,逼你处理闭包陷阱。- Vue 的
computed就是useMemo的自动版:不需要依赖数组、不需要判断「该不该缓存」、不会 stale——因为是框架自动追踪的。 - PureComponent vs React.memo:本质完全相同——PureComponent 是类组件的浅比较版本,React.memo 是函数组件的浅比较版本。
React.StrictMode的双重渲染:开发模式下会故意渲染两次来暴露副作用——如果在 StrictMode 下性能不好,生产模式也好不到哪去。
常见误解(FAQ)
❌ 误区:「给所有组件加 React.memo 就行了」
React.memo 本身有浅比较开销。对简单组件(只渲染几个 <span>),浅比较可能比重新渲染还慢。原则是:先测量,再加 memo——React DevTools Profiler 的 flame graph 告诉你哪些组件在浪费渲染。
❌ 误区:「useMemo 和 useCallback 能提升性能,多加点」
useMemo 不是免费的:1) 需要额外内存存储缓存,2) 每次渲染执行依赖数组比较,3) React 保留随时释放缓存的权力。对小计算(a + b)加 useMemo 是负优化。只在两种场景才加:计算开销确实大(大数据排序/过滤),或作为依赖传给子组件需要稳定引用。
❌ 误区:「Vue 不需要关心渲染性能」
Vue 在大多数情况下默认足够快,但也有需要手动干预的时候:v-for 列表项很复杂时用 v-memo、避免在模板中写复杂计算(放 computed 才用缓存)、大组件拆分成小组件让响应式粒度更细。
❌ 误区:「shouldComponentUpdate 返回 false 就永不更新」
它只跳过当前组件及其子树的虚拟 DOM diff。但如果父组件传入的 props 对象引用变了而值没变,shallowEqual 会认为「变了」——这就是为什么不能直接传内联对象 style={ {color:'red'} }。
一句话总结
React 把「何时跳过渲染」的选择权给了开发者(memo 三件套),Vue 把它留在了框架里(自动追踪 + 编译优化)——前者让你能精确控制但容易漏掉,后者让你省心但难以微调。