React.memo为什么没有用?一个真实的性能优化问题

摘要:我在开发一个数据展示页面时,遇到了性能问题。用户反馈页面在操作时感觉很卡。通过性能分析工具,我发现某个子组件在父组件更新时频繁重新渲染,这显然是不必要的。

我在开发一个数据展示页面时,遇到了性能问题。用户反馈页面在操作时感觉很卡。通过性能分析工具,我发现某个子组件在父组件更新时频繁重新渲染,这显然是不必要的。

作为React开发者,我首先想到了用React.memo来优化性能。但实际使用中,却发现它没有达到预期效果。


问题代码示例

这是我的代码:

const Child = React.memo(({ data }) => {
  console.log("子组件重新渲染了");
  return (
    <div>
      <h3>{data.label}</h3>
      <p>{data.description}</p>
    </div>
  );
});

function App() {
  const [count, setCount] = useState(0);
  
  const data = { label: "用户信息" };

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        点击次数: {count}
      </button>
      <Child data={data} />
    </div>
  );
}


期望与实际结果的差距

我原本的想法是:

  • 点击按钮时,count状态更新

  • 子组件的props看起来没有变化

  • React.memo应该阻止子组件重新渲染

  • 控制台不应该输出重新渲染的信息

但实际情况是:

  • 每次点击按钮

  • 控制台都会输出"子组件重新渲染了"

  • React.memo好像完全没有作用


问题分析过程

看到这个问题,我开始从几个方面思考:

首先,React.memo是如何工作的?它通过比较props的前后值来决定是否重新渲染组件。但它是怎么比较的呢?

其次,在JavaScript中,对象是怎么比较的?两个看起来一样的对象,它们真的相等吗?

最后,每次组件重新渲染时,函数内部的变量会发生什么变化?

为了验证我的想法,我做了一个小实验:

// 测试对象比较
const obj1 = { label: "测试" };
const obj2 = { label: "测试" };
console.log(obj1 === obj2); // 结果是false

// 模拟组件渲染
function testRender() {
  const data = { label: "测试" };
  return data;
}

const result1 = testRender();
const result2 = testRender();
console.log(result1 === result2); // 结果还是false

这个实验让我明白了问题的关键。


问题根源

现在来看这个选择题,哪个选项是正确的?

A. React.memo不能处理对象类型的props,只能处理基本类型
B. data对象在每次渲染时都被重新创建,引用地址改变,导致React.memo认为props发生了变化
C. count状态更新会强制所有子组件重新渲染,React.memo无效
D. console.log语句导致组件每次都会执行

正确答案是B。

解释一下:每次App组件重新渲染时,const data = { label: "用户信息" }这行代码都会执行,创建一个全新的对象。虽然对象的内容相同,但它们在内存中的地址不同。React.memo使用浅比较,发现前后两个data对象的引用不同,就认为props发生了变化,于是重新渲染组件。


其他常见场景

这种问题在实际开发中经常遇到:

场景1:列表渲染

function TodoList() {
  const [todos, setTodos] = useState([]);
  
  return todos.map(todo => (
    <TodoItem 
      key={todo.id}
      todo={todo}
      onDelete={() => handleDelete(todo.id)} // 每次都会创建新函数
    />
  ));
}

场景2:配置传递

function Dashboard() {
  const [refresh, setRefresh] = useState(0);
  
  const chartConfig = {  // 每次渲染都会创建新对象
    width: 800,
    height: 400
  };
  
  return <Chart config={chartConfig} />;
}

场景3:内联样式

function Card({ title }) {
  const style = {  // 每次渲染都会创建新对象
    padding: '20px',
    borderRadius: '8px'
  };
  
  return <div style={style}>{title}</div>;
}


解决方案

知道了问题原因,我们可以这样优化:

  1. 使用useMemo缓存对象

function App() {
  const [count, setCount] = useState(0);
  
  const data = useMemo(() => ({
    label: "用户信息"
  }), []); // 依赖数组为空,只在初始化时创建一次

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        点击次数: {count}
      </button>
      <Child data={data} />
    </div>
  );
}
  1. 将对象移到组件外部

const staticData = { label: "用户信息" };

function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        点击次数: {count}
      </button>
      <Child data={staticData} />
    </div>
  );
}
  1. 使用自定义比较函数

const Child = React.memo(({ data }) => {
  console.log("子组件重新渲染了");
  return <div>{data.label}</div>;
}, (prevProps, nextProps) => {
  // 自定义比较逻辑
  return prevProps.data.label === nextProps.data.label;
});


实际验证

你可以在本地运行这段代码来验证:

import React, { useState, memo } from 'react';

const Child = memo(({ data }) => {
  console.log('子组件渲染了, 对象引用:', data);
  return <div>{data.label}</div>;
});

function App() {
  const [count, setCount] = useState(0);

  const data = { label: "测试" };
  console.log('父组件渲染了, 对象引用:', data);

  return (
    <>
      <button onClick={() => setCount(count + 1)}>
        点击: {count}
      </button>
      <Child data={data} />
    </>
  );
}

观察控制台输出的对象引用,你会发现每次渲染时data对象的引用都在变化。


总结

React.memo失效的根本原因是JavaScript的对象引用特性。要正确使用React.memo,需要注意:

  1. 避免在组件内部创建新的对象、数组或函数

  2. 使用useMemo、useCallback来缓存引用类型

  3. 对于不会变化的数据,可以移到组件外部

  4. 在必要时使用自定义比较函数

理解这些原理后,你就能更好地使用React.memo来优化应用性能,避免不必要的重新渲染。

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://shenqiku.cn/article/FLY_13012