最大的 React 性能杀手不就是你?
你好,我是冴羽。 你写的 React.memo 可能根本没用! 你可能觉得自己已经做了性能优化——给组件包了 但如果你在传递 props 时写成这样: 恭喜你,你的优化白做了~ 为什么呢? 因为 React 比较的是引用,不是内容。 当你写 如果所有 props 都“相等”, React 就跳过子组件的渲染,直接复用上次的结果。 问题来了——React 用什么判断“相等”? 答案是 即使内容完全一样,引用不同就是不同。 React 就会认为 props 变了,然后重新渲染子组件。 这就是为什么内联对象和回调函数是隐藏的性能杀手——它们每次渲染都会创建新引用,让 以前你以为 我得先说清楚:不是所有内联 props 都是性能 bug。 如果子组件很轻量,渲染频率很低,也没用 memo,那内联写法完全没问题。 React 官方文档也一直强调:先测量,别瞎优化。 但当这三个条件同时出现时,你就要注意了: 在这种场景下,不稳定的内联引用不是“增加一点开销”——它会直接废掉了你精心设计的优化! 更要命的是,这个问题不会报错,不会警告,UI 照常工作。 它只会悄悄地让你的列表过滤器变卡、输入延迟、火焰图爆炸。 为了证明这个问题有多严重,我搭了个测试场景: 代码长这样: 结果惨不忍睹: 也就是说, 因为 我还用了 这就是引用不匹配的铁证。 要让 React 的 bailout 机制生效,你需要稳定的引用。 修复后的效果: 性能直接提升 40 倍! 如果子组件依赖引用相等来跳过渲染,那父组件就必须传稳定的引用。如果子组件根本没 memo,或者渲染成本很低,那你稳定引用也没意义。 我的建议是: React 官方文档也是这么说的:能外提就外提,需要缓存再用 Hooks。 关于 React Compiler: 你可能听说过 React Compiler 会自动帮你做这些优化。确实,它能在编译时自动 memoize 很多代码,减少手动写 但这不意味着引用稳定性就不重要了。React Compiler 的文档也说了: 所以即使用了 Compiler,理解引用不稳定如何影响重新渲染,依然是必修课。 内联对象和内联回调不是“错误代码”。大部分时候,它们就是普通的 JSX 表达式。 但当它们穿过 memo 边界时,游戏规则就变了。 这个问题值得更多关注,因为它太容易在生产环境里悄悄发生——代码看起来很干净,应用运行正常,但你以为买到的性能优化其实根本没生效。 所以给想写快速 React 应用的团队一个建议: 先 Profile。如果 memo 的子树还在频繁渲染,先检查 props,别急着怪 React。把静态对象移出渲染路径。只在子组件真正受益时才 memoize 回调。用 React DevTools 和 Why Did You Render 确认到底什么变了、为什么变。 坚持这么做, 我是冴羽,10 年笔耕不辍,专注前端领域,更新了 10+ 系列、300+ 篇原创技术文章,翻译过 Svelte、Solid.js、TypeScript 文档,著有小册《Next.js 开发指南》、《Svelte 开发指南》、《Astro 实战指南》。 欢迎围观我的“网页版朋友圈“,关注我的公众号:冴羽(或搜索 yayujs),每天分享前端知识、AI 干货。React.memo,给回调加了 useCallback,给计算值用了 useMemo。<UserCard
style={{ padding: 16, borderRadius: 8 }}
onSelect={() => handleSelect(user.id)}
config={{ showAvatar: true, compact: false }}
user={user}
/>1. 为什么 React.memo 会失效?
React.memo 包裹一个组件时,React 会在父组件重新渲染时比较新旧 props。Object.is,也就是引用相等:Object.is({ padding: 16 }, { padding: 16 }) // false
Object.is(() => {}, () => {}) // falseReact.memo 形同虚设。React.memo 在帮你省性能,实际上,每次都在重新渲染。2. 什么时候这个问题会要命?
3. 我做了个实验:200 行列表的性能崩溃
React.memo 包裹的 ProductRow 组件{filteredProducts.map(p => (
<ProductRow
key={p.id}
product={p}
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '12px 20px',
borderBottom: '1px solid #eee'
}}
onAddToCart={(id) => console.log('Added:', id)}
/>
))}Renders: 14React.memo 完全失效了。style 对象和 onAddToCart 回调每次都是新创建的,memo 的 props 比较每次都是失败的。why-did-you-render 这个工具来诊断。它直接告诉我:props.style 是“内容相同但对象不同”props.onAddToCart 是“同名但函数不同”4. 怎么改?很简单
改法 1:把静态对象移到模块作用域
// ✅ 在组件外部定义,只创建一次
const ROW_STYLE = {
display: 'flex',
justifyContent: 'space-between',
padding: '12px 20px',
borderBottom: '1px solid #eee'
};
export default function ProductList() {
// ...
return (
<ProductRow
style={ROW_STYLE}
// ...
/>
);
}改法 2:用 useCallback 包裹动态回调
export default function ProductList() {
const [searchTerm, setSearchTerm] = useState('');
// ✅ 依赖数组为空,函数引用保持稳定
const handleAddToCart = useCallback((id) => {
console.log('Added:', id);
}, []);
return (
<ProductRow
onAddToCart={handleAddToCart}
// ...
/>
);
}ProductList 渲染时间从 243.9ms 降到 6ms2why-did-you-render 不再报警5.所以什么时候该优化,什么时候别管?
useCallback 和 useMemouseMemo 和 useCallback 的需求。useMemo 和 useCallback 在某些场景下仍然有用,比如你需要精确控制 Effect 的依赖。6. 最后一句话
React.memo** 就不再是装饰性的性能代码,而是真正在干活。**