8、React 性能优化实战

3 阅读4分钟

—— 让你的应用更快、更流畅

💡 “过早优化是万恶之源”,但“关键路径上的性能优化”是专业开发者必备技能。
我们的目标不是盲目优化,而是 精准识别瓶颈 + 合理使用工具


一、为什么需要性能优化?📉

虽然 React 的虚拟 DOM 已经很高效,但在以下场景仍可能出现卡顿:

场景问题
列表渲染上千条数据页面卡顿、滚动不流畅
频繁状态更新不必要的重渲染(re-render)
复杂计算每次重新执行拖慢交互响应
组件过度嵌套更新传播链太长

✅ 优化目标:

  • 减少不必要的渲染
  • 延迟加载非关键资源
  • 缓存昂贵的计算结果

二、核心工具:React.memo / useMemo / useCallback 🛠️

这三个是 React 提供的性能优化原语,它们都基于“记忆化(Memoization)”思想 —— 避免重复工作

1. React.memo:防止组件不必要渲染 ✅

默认行为:父组件更新 → 所有子组件默认也更新

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

  return (
    <div>
      <h2>计数器: {count}</h2>
      <button onClick={() => setCount(count + 1)}>+1</button>
      {/* 这个组件其实不需要重新渲染 */}
      <ExpensiveComponent data={veryLargeData} />
    </div>
  );
}

👉 即使 ExpensiveComponentdata 没变,它也会跟着父组件一起重渲染!

解决方案:用 React.memo 包裹

const ExpensiveComponent = React.memo(function ({ data }) {
  // 复杂渲染逻辑
  return <div>{/* 渲染大量内容 */}</div>;
});

React.memo 会浅比较 props,如果没变化,就跳过渲染。

自定义比较函数(进阶)

const MemoizedComponent = React.memo(
  ({ a, b }) => {
    /* render */
  },
  (prevProps, nextProps) => {
    // 返回 true 表示“相同”,不重新渲染
    return prevProps.a === nextProps.a && prevProps.b.id === nextProps.b.id;
  }
);

2. useMemo:缓存昂贵的计算结果 💾

❌ 错误写法:每次渲染都重新计算

function MyComponent({ list, filterText }) {
  // 每次渲染都会执行这个耗时操作!
  const filtered = expensiveFilter(list, filterText);

  return (
    <ul>
      {filtered.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
}

✅ 正确做法:使用 useMemo

function MyComponent({ list, filterText }) {
  const filtered = useMemo(() => {
    return expensiveFilter(list, filterText);
  }, [list, filterText]); // 依赖项不变时,复用上次结果

  return (
    <ul>
      {filtered.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
}

✅ 只有当 listfilterText 改变时才重新计算。


3. useCallback:缓存函数引用 🔗

问题:子组件频繁重渲染

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

  // 每次渲染都会创建新函数 → 引用改变
  const handleSave = () => {
    console.log('保存数据');
  };

  return (
    <div>
      <Child onSave={handleSave} /> {/* 引用变了 → Child 被迫重渲染 */}
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

✅ 使用 useCallback 缓存函数

const handleSave = useCallback(() => {
  console.log('保存数据');
}, []); // 空依赖数组 → 函数在整个生命周期中保持不变

✅ 结合 React.memo 使用效果最佳:

const Child = React.memo(({ onSave }) => {
  console.log('Child 渲染了一次'); // 仅在 onSave 变化时打印
  return <button onClick={onSave}>保存</button>;
});

三、懒加载与代码分割 🧩

1. 路由级懒加载(前面已讲)

const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));

<Suspense fallback={<Spinner />}>
  <Routes>
    <Route path="/" element={<Home />} />
    <Route path="/about" element={<About />} />
  </Routes>
</Suspense>

✅ Webpack 会自动拆分代码包,按需加载。


2. 组件级懒加载(动态导入)

适用于模态框、复杂图表等重型组件:

function Dashboard() {
  const [showChart, setShowChart] = useState(false);

  return (
    <div>
      <button onClick={() => setShowChart(true)}>查看图表</button>
      {showChart && (
        <Suspense fallback="加载中...">
          <LazyChartComponent />
        </Suspense>
      )}
    </div>
  );
}

const LazyChartComponent = lazy(() => import('./Chart'));

四、使用 Profiler 分析性能 🔍

React 提供了 <Profiler> 来测量组件渲染成本。

function onRender(id, phase, actualDuration) {
  console.log({
    组件: id,
    阶段: phase, // 'mount' | 'update'
    渲染耗时: actualDuration, // 毫秒
  });
}

// 在任意位置包裹
<Profiler id="Dashboard" onRender={onRender}>
  <Dashboard />
</Profiler>

✅ 推荐在开发环境开启,找出耗时高的组件。


五、常见反模式与避坑指南 ⚠️

❌ 反模式 1:滥用 memo / useMemo

// ❌ 没必要!简单组件或小对象没必要缓存
const label = useMemo(() => <span>姓名</span>, []);

// ❌ 对象/函数在依赖项中每次都新建
useEffect(() => {
  // ...
}, [{ name }]); // 每次都是新对象!

// ✅ 应该提取或使用 useRef

✅ 规则:只在真正昂贵的操作上使用 useMemo / useCallback


❌ 反模式 2:在循环中使用 memo

{items.map(item => (
  <React.memo key={item.id} component={Row} /> // ❌ 无效!
))}

✅ 正确做法:

const Row = React.memo(({ item }) => <div>{item.name}</div>);

{items.map(item => (
  <Row key={item.id} item={item} /> // ✅ 正确使用
))}

六、实战练习 🏋️‍♂️

✅ 练习 1:优化一个低效列表

有一个用户列表,每行有一个“编辑”按钮,点击弹窗。

  • 问题:点击“编辑”导致整个列表重渲染。
  • 目标:使用 React.memo + useCallback 修复。
function UserList({ users }) {
  const [editingId, setEditingId] = useState(null);

  const handleEdit = useCallback((id) => {
    setEditingId(id);
  }, []);

  return (
    <div>
      {users.map(user => (
        <UserRow
          key={user.id}
          user={user}
          onEdit={handleEdit}
          isEditing={editingId === user.id}
        />
      ))}
    </div>
  );
}

const UserRow = React.memo(({ user, onEdit, isEditing }) => {
  console.log('渲染:', user.name);
  return (
    <div>
      {user.name} - {user.email}
      <button onClick={() => onEdit(user.id)}>
        {isEditing ? '正在编辑...' : '编辑'}
      </button>
    </div>
  );
});

✅ 测试:点击编辑后,其他行不再重复打印日志。


✅ 练习 2:缓存过滤后的商品列表

function ProductList({ products, category, searchQuery }) {
  const filtered = useMemo(() => {
    return products
      .filter(p => p.category === category)
      .filter(p => p.name.includes(searchQuery));
  }, [products, category, searchQuery]);

  return (
    <ul>
      {filtered.map(p => <li key={p.id}>{p.name}</li>)}
    </ul>
  );
}

✅ 加入 console.log('执行过滤') 验证是否只在依赖变化时运行。


✅ 总结:性能优化核心原则

技术适用场景是否常用
React.memo子组件 props 不常变✅ 高频
useCallback传递给子组件的回调函数✅ 高频
useMemo昂贵计算、大型列表过滤✅ 中频
lazy + Suspense路由/重型组件懒加载✅ 必备
<Profiler>性能分析定位瓶颈✅ 开发期使用

🎯 记住一句话:

  • 先测量,再优化
  • 不要过度优化简单组件
  • 优先优化用户可感知的部分(如滚动、输入响应)

🎯 下一步预告
你已经能让应用又快又好!接下来我们要进入现代 React 的巅峰:

➡️ 自定义 Hook + 最佳实践架构
—— 封装通用能力,打造可维护、可扩展的项目结构

是否继续?我将带你写出“大厂级别”的 React 架构设计。