useMemo
useMemo 的作用 / 解决了什么问题?
useMemo 是 React 的一个性能优化 Hook,主要用于解决以下问题:
避免重复计算
- 对于计算量大的操作,避免在每次渲染时都重新计算
- 缓存计算结果,只在依赖项变化时重新计算
防止不必要的渲染
- 对于复杂对象或数组,避免每次渲染都创建新的引用
- 特别是当这些值被用作子组件的 props 时
useMemo 基本用法
jsx
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
基本语法说明
- 第一个参数:工厂函数,返回需要缓存的值
- 第二个参数:依赖项数组,只有依赖项变化时才重新计算
使用场景
1. 复杂计算优化
jsx
function ProductList({ products, filter }) {
const filteredProducts = useMemo(() => {
return products.filter(product => {
// 复杂的过滤逻辑
return product.price > filter.minPrice &&
product.price < filter.maxPrice &&
product.category === filter.category &&
product.name.includes(filter.searchText);
}).sort((a, b) => b.price - a.price); // 按价格排序
}, [products, filter]);
return (
<ul>
{filteredProducts.map(product => (
<li key={product.id}>
{product.name} - ${product.price}
</li>
))}
</ul>
);
}
使用 useMemo 的效果
- 过滤和排序的结果被缓存,只在 products 或 filter 变化时重新计算
- 当组件因其他状态变化重新渲染时,不会重复执行昂贵的计算
- 在大数据集和复杂计算场景下,可以显著提升性能
- 用户界面更流畅,没有明显的卡顿
不使用 useMemo 的后果
jsx
function ProductList({ products, filter }) {
// 每次渲染都会重新计算
const filteredProducts = products
.filter(product => {
return product.price > filter.minPrice &&
product.price < filter.maxPrice &&
product.category === filter.category &&
product.name.includes(filter.searchText);
})
.sort((a, b) => b.price - a.price);
return (
<ul>
{filteredProducts.map(product => (
<li key={product.id}>
{product.name} - ${product.price}
</li>
))}
</ul>
);
}
- 每次组件重新渲染都会重新执行过滤和排序
- 在产品列表较大时,可能导致明显的性能问题
- 即使 filter 条件没有变化,也会重复计算
- 可能导致用户界面卡顿,特别是在处理大量数据时
2. 引用相等性优化
jsx
function ChartComponent({ data, config }) {
const memoizedConfig = useMemo(() => ({
type: 'line',
options: {
responsive: true,
scales: config.scales,
animations: config.animations,
plugins: {
tooltip: config.tooltip,
legend: config.legend
}
}
}), [config.scales, config.animations, config.tooltip, config.legend]);
return (
<Chart
data={data}
config={memoizedConfig}
/>
);
}
使用 useMemo 的效果
- 配置对象的引用保持稳定,只在相关配置真正变化时才更新
- 避免子组件(Chart)不必要的重新渲染
- 特别适合用于配置复杂的第三方组件
- 提升图表等重型组件的渲染性能
不使用 useMemo 的后果
jsx
function ChartComponent({ data, config }) {
// 每次渲染都创建新的配置对象
const chartConfig = {
type: 'line',
options: {
responsive: true,
scales: config.scales,
animations: config.animations,
plugins: {
tooltip: config.tooltip,
legend: config.legend
}
}
};
return (
<Chart
data={data}
config={chartConfig}
/>
);
}
- 每次渲染都创建新的配置对象,即使配置内容没有变化
- 导致 Chart 组件不必要的重新渲染
- 可能触发图表的重新计算和动画
- 在复杂的数据可视化场景下性能影响明显
3. 大型列表渲染优化
jsx
function VirtualizedList({ items, filterText }) {
const filteredItems = useMemo(() => {
console.log('Computing filtered items');
return items
.filter(item => item.text.toLowerCase().includes(filterText.toLowerCase()))
.map(item => ({
...item,
highlight: getHighlightRanges(item.text, filterText)
}));
}, [items, filterText]);
return (
<VirtualScroller
items={filteredItems}
itemHeight={50}
renderItem={({ item }) => (
<ListItem
key={item.id}
text={item.text}
highlights={item.highlight}
/>
)}
/>
);
}
使用 useMemo 的效果
- 过滤和高亮计算的结果被缓存,只在数据或过滤条件变化时重新计算
- 虚拟滚动的性能得到优化,滚动更流畅
- 避免在滚动时重新计算过滤和高亮
- 大幅提升用户体验,特别是在处理长列表时
不使用 useMemo 的后果
jsx
function VirtualizedList({ items, filterText }) {
// 每次渲染都重新计算过滤和高亮
const filteredItems = items
.filter(item => item.text.toLowerCase().includes(filterText.toLowerCase()))
.map(item => ({
...item,
highlight: getHighlightRanges(item.text, filterText)
}));
return (
<VirtualScroller
items={filteredItems}
itemHeight={50}
renderItem={({ item }) => (
<ListItem
key={item.id}
text={item.text}
highlights={item.highlight}
/>
)}
/>
);
}
- 每次滚动或其他状态更新都会重新计算过滤和高亮
- 可能导致滚动卡顿和性能问题
- 在列表项较多时,用户体验显著下降
- CPU 使用率升高,可能影响设备电池寿命
4. 动态样式计算
jsx
function DynamicThemeComponent({ theme, dimensions }) {
const styles = useMemo(() => ({
container: {
background: theme.background,
padding: `${dimensions.spacing}px`,
borderRadius: theme.borderRadius,
boxShadow: `0 ${dimensions.elevation}px ${dimensions.elevation * 2}px ${theme.shadowColor}`,
transform: `scale(${dimensions.scale})`,
transition: 'all 0.3s ease'
},
content: {
color: theme.textColor,
fontSize: `${dimensions.fontSize}px`,
lineHeight: dimensions.lineHeight
}
}), [theme, dimensions]);
return (
<div style={styles.container}>
<div style={styles.content}>
Dynamic Themed Content
</div>
</div>
);
}
使用 useMemo 的效果
- 样式对象只在主题或尺寸变化时重新计算
- 避免不必要的样式重新计算和 DOM 更新
- 动画和过渡效果更流畅
- 减少浏览器重排和重绘的次数
不使用 useMemo 的后果
jsx
function DynamicThemeComponent({ theme, dimensions }) {
// 每次渲染都重新创建样式对象
const styles = {
container: {
background: theme.background,
padding: `${dimensions.spacing}px`,
borderRadius: theme.borderRadius,
boxShadow: `0 ${dimensions.elevation}px ${dimensions.elevation * 2}px ${theme.shadowColor}`,
transform: `scale(${dimensions.scale})`,
transition: 'all 0.3s ease'
},
content: {
color: theme.textColor,
fontSize: `${dimensions.fontSize}px`,
lineHeight: dimensions.lineHeight
}
};
return (
<div style={styles.container}>
<div style={styles.content}>
Dynamic Themed Content
</div>
</div>
);
}
- 每次渲染都创建新的样式对象
- 可能触发不必要的 DOM 更新
- 在动画过程中可能出现卡顿
- 影响复杂布局的性能表现
使用注意事项
不要过度使用
- useMemo 本身也有性能开销
- 只在确实需要优化的地方使用
- 对于简单的计算,使用 useMemo 可能会适得其反
正确设置依赖项
- 依赖项数组必须包含所有在回调函数中使用的外部变量
- 避免遗漏依赖项,这可能导致bug
- 考虑使用 ESLint 的 exhaustive-deps 规则
避免过早优化
- 先编写正常工作的代码
- 在发现性能问题时再考虑使用 useMemo
- 使用性能测试工具验证优化效果
性能优化最佳实践
合理的使用时机
jsx// 好的使用场景 const expensiveValue = useMemo(() => { return someVeryExpensiveOperation(props.data); }, [props.data]); // 不必要的使用场景 const simpleValue = useMemo(() => props.value * 2, [props.value]); // 过度优化
结合 React.memo
jsxconst MemoizedChild = React.memo(function Child({ data }) { return <div>{data.value}</div>; }); function Parent() { const memoizedData = useMemo(() => ({ value: 'expensive computation' }), []); return <MemoizedChild data={memoizedData} />; }
与其他 Hooks 的对比
vs useCallback
- useMemo:缓存计算结果
- useCallback:缓存函数引用
jsx// useMemo 缓存值 const value = useMemo(() => computeValue(a, b), [a, b]); // useCallback 缓存函数 const handler = useCallback(() => doSomething(a, b), [a, b]);
vs useState
- useMemo:用于复杂计算的缓存
- useState:用于组件状态管理
jsx// useState 用于简单状态 const [count, setCount] = useState(0); // useMemo 用于复杂计算 const expensiveCount = useMemo(() => calculateExpensiveValue(count), [count]);
实现原理
useMemo 的核心实现原理基于以下几点:
依赖项比较
- 使用 Object.is 比较新旧依赖项
- 只有当依赖项变化时才重新执行工厂函数
缓存机制
- 在 Fiber 节点上维护一个缓存
- 缓存包含上一次的计算结果和依赖项
基本实现示意
javascript
function useMemo(factory, deps) {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
if (hook.memoizedState !== null) {
// 有缓存时,比较依赖项
if (nextDeps !== null) {
const prevDeps = hook.memoizedState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return hook.memoizedState[0];
}
}
}
// 计算新值并缓存
const nextValue = factory();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}