useMemo 与性能优化一、useMemo 基础1.1 什么是 useMemouseMemo 是一个性能优化的 Hook用于缓存计算结果避免在每次渲染时重新执行昂贵的计算。1.2 基本语法const memoizedValue useMemo(() computeExpensiveValue(a, b), [a, b]);第一个参数计算函数第二个参数依赖数组返回值缓存的计算结果1.3 基础示例function ExpensiveComponent({ numbers }) { // ❌ 每次渲染都会重新计算 const sum numbers.reduce((a, b) a b, 0); // ✅ 只在 numbers 变化时重新计算 const memoizedSum useMemo(() { console.log(重新计算总和...); return numbers.reduce((a, b) a b, 0); }, [numbers]); return div总和: {memoizedSum}/div; }二、何时使用 useMemo2.1 昂贵的计算function DataTable({ data, filter }) { // 昂贵的数据处理 const processedData useMemo(() { console.log(处理数据...); return data .filter(item item.category filter) .sort((a, b) b.score - a.score) .map(item ({ ...item, formattedScore: item.score.toFixed(2) })); }, [data, filter]); return Table data{processedData} /; }2.2 复杂对象计算function ChartComponent({ data, config }) { // 图表数据转换 const chartData useMemo(() { return { labels: data.map(item item.label), datasets: [{ label: config.label, data: data.map(item item.value), backgroundColor: config.color }] }; }, [data, config.label, config.color]); return Chart data{chartData} /; }2.3 递归或循环计算function Fibonacci({ n }) { // 递归计算斐波那契数列 const fib useMemo(() { const calculate (num) { if (num 1) return num; return calculate(num - 1) calculate(num - 2); }; return calculate(n); }, [n]); return divFibonacci({n}) {fib}/div; }三、useMemo vs 普通计算3.1 性能对比function PerformanceComparison() { const [count, setCount] useState(0); const [text, setText] useState(); // 模拟昂贵计算 const expensiveCalculation (num) { console.log(执行昂贵计算...); let result 0; for (let i 0; i 100000000; i) { result i; } return result num; }; // ❌ 每次渲染都执行包括输入文本时 const badResult expensiveCalculation(count); // ✅ 只在 count 变化时执行 const goodResult useMemo(() expensiveCalculation(count), [count]); return ( div p结果: {goodResult}/p button onClick{() setCount(count 1)}增加计数/button input value{text} onChange{(e) setText(e.target.value)} / /div ); }3.2 引用相等性function Parent() { const [count, setCount] useState(0); // ❌ 每次渲染都创建新对象 const config { theme: dark, size: large }; // ✅ 只在依赖变化时创建新对象 const memoizedConfig useMemo(() ({ theme: dark, size: large }), []); return ( div Child config{memoizedConfig} / button onClick{() setCount(count 1)}点击/button /div ); } // 使用 React.memo 优化的子组件 const Child React.memo(({ config }) { console.log(Child 渲染); return div{config.theme}/div; });四、实际应用场景4.1 搜索过滤function SearchList({ items, searchTerm }) { const filteredItems useMemo(() { console.log(过滤数据...); return items.filter(item item.name.toLowerCase().includes(searchTerm.toLowerCase()) ); }, [items, searchTerm]); return ( ul {filteredItems.map(item ( li key{item.id}{item.name}/li ))} /ul ); }4.2 数据分组function GroupedData({ transactions }) { const groupedData useMemo(() { return transactions.reduce((groups, transaction) { const date transaction.date.split(-)[0]; // 按年份分组 if (!groups[date]) { groups[date] []; } groups[date].push(transaction); return groups; }, {}); }, [transactions]); return ( div {Object.entries(groupedData).map(([year, items]) ( div key{year} h3{year}/h3 {items.map(item ( div key{item.id}{item.description}: ${item.amount}/div ))} /div ))} /div ); }4.3 表单验证function RegistrationForm() { const [formData, setFormData] useState({ username: , email: , password: , confirmPassword: }); const errors useMemo(() { const newErrors {}; if (!formData.username) { newErrors.username 用户名不能为空; } else if (formData.username.length 3) { newErrors.username 用户名至少3个字符; } if (!formData.email) { newErrors.email 邮箱不能为空; } else if (!/\S\S\.\S/.test(formData.email)) { newErrors.email 邮箱格式不正确; } if (!formData.password) { newErrors.password 密码不能为空; } else if (formData.password.length 6) { newErrors.password 密码至少6位; } if (formData.password ! formData.confirmPassword) { newErrors.confirmPassword 两次输入的密码不一致; } return newErrors; }, [formData]); const isValid useMemo(() Object.keys(errors).length 0, [errors]); return ( form input nameusername value{formData.username} onChange{(e) setFormData(prev ({ ...prev, username: e.target.value }))} / {errors.username span classNameerror{errors.username}/span} {/* 其他字段类似 */} button typesubmit disabled{!isValid}注册/button /form ); }4.4 派生状态function ShoppingCart({ items }) { // 派生状态总价、总数量等 const summary useMemo(() { const totalItems items.reduce((sum, item) sum item.quantity, 0); const subtotal items.reduce((sum, item) sum item.price * item.quantity, 0); const tax subtotal * 0.1; const total subtotal tax; return { totalItems, subtotal, tax, total }; }, [items]); return ( div p商品总数: {summary.totalItems}/p p小计: ${summary.subtotal}/p p税费: ${summary.tax}/p p总计: ${summary.total}/p /div ); }五、性能优化最佳实践5.1 不要过度使用// ❌ 不必要的 useMemo简单计算 const doubled useMemo(() count * 2, [count]); // ✅ 直接计算 const doubled count * 2; // ❌ 不必要的 useMemo原始值 const name useMemo(() John, []); // ✅ 直接使用常量 const name John;5.2 正确设置依赖function CorrectDeps({ userId, onComplete }) { // ✅ 包含所有依赖 const userData useMemo(() { return fetchUserData(userId); }, [userId]); // ✅ 函数依赖也需要包含 const handleClick useCallback(() { onComplete?.(); }, [onComplete]); }5.3 结合 React.memo// 父组件 function Parent({ data }) { const processedData useMemo(() processData(data), [data]); return ExpensiveChild data{processedData} /; } // 子组件 const ExpensiveChild React.memo(({ data }) { // 只在 data 真正变化时重渲染 return div{/* 复杂渲染 */}/div; });六、常见陷阱6.1 依赖数组遗漏function BadMemo({ items, filter }) { // ❌ 缺少 filter 依赖 const filtered useMemo(() { return items.filter(item item.category filter); }, [items]); // filter 变化时不会更新 // ✅ 正确 const correctFiltered useMemo(() { return items.filter(item item.category filter); }, [items, filter]); }6.2 在 useMemo 中执行副作用// ❌ 错误useMemo 不应该有副作用 const result useMemo(() { console.log(计算中); // 副作用 fetchData(); // 副作用 return compute(); }, [deps]); // ✅ 使用 useEffect 处理副作用 useEffect(() { console.log(计算中); fetchData(); }, [deps]); const result useMemo(() compute(), [deps]);6.3 内存泄漏function MemoryLeak() { const [data, setData] useState(null); // ❌ 每次依赖变化都创建新的大对象 const bigObject useMemo(() { return createHugeArray(); // 可能占用大量内存 }, [data]); // ✅ 考虑是否需要缓存大对象 const bigObject useMemo(() { return createHugeArray(); }, [data?.id]); // 只在必要时重新创建 }七、useMemo vs useCallback特性useMemouseCallback返回值计算值函数用途缓存计算结果缓存函数引用语法useMemo(() value, deps)useCallback(fn, deps)等价-useMemo(() fn, deps)// useCallback 等价写法 const memoizedCallback useCallback(() { doSomething(a, b); }, [a, b]); // 等同于 const memoizedCallback useMemo(() { return () doSomething(a, b); }, [a, b]);八、性能测量8.1 使用 React DevTools// 在开发环境中添加性能标记 function MeasuredComponent({ data }) { const start performance.now(); const processed useMemo(() { const result expensiveOperation(data); const end performance.now(); console.log(处理耗时: ${end - start}ms); return result; }, [data]); return div{processed}/div; }8.2 使用 why-did-you-rendernpminstallwelldone-software/why-did-you-renderimport React from react; if (process.env.NODE_ENV development) { const whyDidYouRender require(welldone-software/why-did-you-render); whyDidYouRender(React, { trackAllPureComponents: true, }); }九、练习题基础题实现一个组件对大数组进行排序和过滤使用 useMemo 优化实现一个购物车计算总价时使用 useMemo进阶题实现一个带有搜索和筛选的产品列表实现一个数据表格支持排序和分页参考答案// 产品列表带搜索和筛选 function ProductList({ products, category, minPrice, searchTerm }) { const filteredProducts useMemo(() { console.log(过滤产品...); return products .filter(product { if (category product.category ! category) return false; if (minPrice product.price minPrice) return false; if (searchTerm !product.name.toLowerCase().includes(searchTerm.toLowerCase())) return false; return true; }) .sort((a, b) a.price - b.price); }, [products, category, minPrice, searchTerm]); return ( div {filteredProducts.map(product ( div key{product.id} {product.name} - ${product.price} /div ))} /div ); }十、小结要点说明适用场景昂贵计算、对象/数组稳定性不适用场景简单计算、原始值依赖数组必须正确声明所有依赖性能权衡useMemo 本身也有开销核心要点useMemo 用于缓存计算结果不要过度优化先测量再优化正确设置依赖数组配合 React.memo 使用效果更好