Skip to content

最大的 React 性能杀手不就是你? #429

@mqyqingfeng

Description

@mqyqingfeng

你好,我是冴羽

你写的 React.memo 可能根本没用!

你可能觉得自己已经做了性能优化——给组件包了 React.memo,给回调加了 useCallback,给计算值用了 useMemo

但如果你在传递 props 时写成这样:

<UserCard
  style={{ padding: 16, borderRadius: 8 }}
  onSelect={() => handleSelect(user.id)}
  config={{ showAvatar: true, compact: false }}
  user={user}
/>

恭喜你,你的优化白做了~

1. 为什么 React.memo 会失效?

为什么呢?

因为 React 比较的是引用,不是内容。

当你写 React.memo 包裹一个组件时,React 会在父组件重新渲染时比较新旧 props。

如果所有 props 都“相等”, React 就跳过子组件的渲染,直接复用上次的结果。

问题来了——React 用什么判断“相等”?

答案是 Object.is,也就是引用相等:

Object.is({ padding: 16 }, { padding: 16 }) // false
Object.is(() => {}, () => {}) // false

即使内容完全一样,引用不同就是不同。 React 就会认为 props 变了,然后重新渲染子组件。

这就是为什么内联对象和回调函数是隐藏的性能杀手——它们每次渲染都会创建新引用,让 React.memo 形同虚设。

以前你以为 React.memo 在帮你省性能,实际上,每次都在重新渲染。

2. 什么时候这个问题会要命?

我得先说清楚:不是所有内联 props 都是性能 bug。

如果子组件很轻量,渲染频率很低,也没用 memo,那内联写法完全没问题。

React 官方文档也一直强调:先测量,别瞎优化

但当这三个条件同时出现时,你就要注意了:

  1. 父组件频繁重新渲染(比如搜索输入、滚动状态、筛选器)

  2. 子组件或子树足够大,重新渲染的成本很高

  3. 你已经加了 memo,期待 React 跳过不必要的渲染

在这种场景下,不稳定的内联引用不是“增加一点开销”——它会直接废掉了你精心设计的优化!

更要命的是,这个问题不会报错,不会警告,UI 照常工作。

它只会悄悄地让你的列表过滤器变卡、输入延迟、火焰图爆炸。

3. 我做了个实验:200 行列表的性能崩溃

为了证明这个问题有多严重,我搭了个测试场景:

  • 一个可搜索的商品列表

  • 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)}
  />
))}

结果惨不忍睹:

  • 输入 6 个字符后,每个可见行显示 Renders: 14

  • React DevTools Profiler 显示:单次按键触发的渲染耗时 243.9ms

  • 火焰图里,所有 200 个子组件全部点亮

也就是说,React.memo 完全失效了。

因为 style 对象和 onAddToCart 回调每次都是新创建的,memo 的 props 比较每次都是失败的。

Browser window showing the ProductRow list with Render count badges.

React DevTools Profiler tab showing a Flamegraph for ProductList re-processing.

我还用了 why-did-you-render 这个工具来诊断。它直接告诉我:

  • props.style 是“内容相同但对象不同”

  • props.onAddToCart 是“同名但函数不同”

这就是引用不匹配的铁证。

Browser Console output from why-did-you-render confirming reference mismatch.

4. 怎么改?很简单

要让 React 的 bailout 机制生效,你需要稳定的引用

改法 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 降到 6ms

  • 无论怎么输入,渲染计数始终停在 2

  • why-did-you-render 不再报警

性能直接提升 40 倍

React DevTools Profiler after fix showing ProductList at 6ms

App UI showing nonchanging Render count despite active searching

5.所以什么时候该优化,什么时候别管?

如果子组件依赖引用相等来跳过渲染,那父组件就必须传稳定的引用。如果子组件根本没 memo,或者渲染成本很低,那你稳定引用也没意义。

我的建议是:

  • 静态值优先外提: 如果一个对象永远不变,把它移到组件外部,零成本解决问题

  • 动态值按需 memo: 只在子组件真的能从稳定引用中受益时,才用 useCallbackuseMemo

  • 先 Profile 再优化: 别瞎猜,用 React DevTools Profiler 测一下再说

React 官方文档也是这么说的:能外提就外提,需要缓存再用 Hooks。

关于 React Compiler:

你可能听说过 React Compiler 会自动帮你做这些优化。确实,它能在编译时自动 memoize 很多代码,减少手动写 useMemouseCallback 的需求。

但这不意味着引用稳定性就不重要了。React Compiler 的文档也说了:useMemouseCallback 在某些场景下仍然有用,比如你需要精确控制 Effect 的依赖。

所以即使用了 Compiler,理解引用不稳定如何影响重新渲染,依然是必修课。

6. 最后一句话

内联对象和内联回调不是“错误代码”。大部分时候,它们就是普通的 JSX 表达式。

但当它们穿过 memo 边界时,游戏规则就变了。

这个问题值得更多关注,因为它太容易在生产环境里悄悄发生——代码看起来很干净,应用运行正常,但你以为买到的性能优化其实根本没生效。

所以给想写快速 React 应用的团队一个建议:

先 Profile。如果 memo 的子树还在频繁渲染,先检查 props,别急着怪 React。把静态对象移出渲染路径。只在子组件真正受益时才 memoize 回调。用 React DevTools 和 Why Did You Render 确认到底什么变了、为什么变。

坚持这么做,React.memo** 就不再是装饰性的性能代码,而是真正在干活。**

我是冴羽,10 年笔耕不辍,专注前端领域,更新了 10+ 系列、300+ 篇原创技术文章,翻译过 Svelte、Solid.js、TypeScript 文档,著有小册《Next.js 开发指南》、《Svelte 开发指南》、《Astro 实战指南》。

欢迎围观我的“网页版朋友圈“,关注我的公众号:冴羽(或搜索 yayujs),每天分享前端知识、AI 干货。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions