Skip to content
性能优化

性能优化

React 默认渲染模型:当组件状态变化时,它和它所有子组件都会重新渲染。大多数情况下这不是问题,但大列表、高频更新场景需要优化。


优化原则

💡 先让应用可运行,再优化。不要过早优化——React 的默认渲染已经足够快,大多数应用无需手动优化。只在确实卡顿时分析、定位、优化。

步骤 工具
1. 定位性能瓶颈 React DevTools Profiler
2. 确认具体问题 是渲染太多?计算太重?还是网络慢?
3. 针对性优化 跳过渲染、缓存计算、代码分割

React.memo — 跳过不必要的渲染

const ExpensiveChild = React.memo(function ExpensiveChild({ name, age }) {
  console.log('ExpensiveChild 渲染');
  return <div>{name}: {age}</div>;
});

// 当 name 和 age 都不变时,跳过渲染

memo 的工作原理

父组件重渲染
  ↓ React 想要重渲染子组件
React.memo 拦截
  ↓ 浅比较新旧 props
  props 相同?
  ├── YES → 跳过渲染 ✅
  └── NO → 正常渲染

自定义比较函数

const areEqual = (prevProps, nextProps) => {
  return prevProps.user.id === nextProps.user.id;
};

export default React.memo(UserCard, areEqual);

🚨 陷阱:不要把所有组件都包上 memo。只有当组件符合以下条件时:

  • 组件渲染成本高(有大量 DOM 输出)
  • 在同一父组件下频繁渲染
  • 传入的 props 在大部分渲染中不变

为什么 memo 常失效

// ❌ memo 无效:每次渲染传入新对象
<MemoChild style={{ color: 'red' }} />
<MemoChild onClick={() => doSomething()} />

// ✅ 稳定引用
const style = useMemo(() => ({ color: 'red' }), []);
const onClick = useCallback(() => doSomething(), []);
<MemoChild style={style} onClick={onClick} />

useMemo & useCallback

// useMemo:缓存值(避免重复计算)
const filtered = useMemo(
  () => items.filter(i => i.active).sort(byPriority),
  [items]
);

// useCallback:缓存函数引用(配合 memo 使用)
const handleSelect = useCallback((id) => {
  setSelected(id);
}, []);

性能决策表

场景 是否需要
items.filter().sort() — items 长度 < 100 ❌ 不需要
同上 — items 长度 > 10000 ✅ 用 useMemo
a + b — 加法 ❌ 不需要
传给普通 <div> 的事件处理函数 ❌ 没必要用 useCallback
传给 memo(Child) 的事件处理函数且 Child 渲染代价大 ✅ 用 useCallback
作为 useEffect 的依赖 ✅ 用 useCallback/useMemo

💡 最佳实践:useMemo/useCallback 本身也有开销(缓存存储、依赖数组比较)。只在实际有益时使用。


代码分割(Code Splitting)

路由级分割

import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Analytics = lazy(() => import('./pages/Analytics'));

function App() {
  return (
    <Suspense fallback={<PageLoader />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/analytics" element={<Analytics />} />
      </Routes>
    </Suspense>
  );
}

条件分割(按需加载)

const MarkdownEditor = lazy(() => import('./MarkdownEditor'));

function App() {
  const [showEditor, setShowEditor] = useState(false);
  return (
    <div>
      <button onClick={() => setShowEditor(true)}>编辑</button>
      {showEditor && (
        <Suspense fallback={<Loading />}>
          <MarkdownEditor />
        </Suspense>
      )}
    </div>
  );
}

lazy 的限制

  • 只能用默认导出(export default)—— 命名导出需做中间层
  • 不能用在服务端渲染中(需要 Next.js 的 next/dynamic

虚拟列表(Windowing)

当列表项成千上万时,只渲染可见区域的 DOM:

npm install @tanstack/react-virtual
import { useVirtualizer } from '@tanstack/react-virtual';

function BigList({ items }) {
  const parentRef = useRef(null);

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 40,
  });

  return (
    <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
      <div style={{ height: `${virtualizer.getTotalSize()}px` }}>
        {virtualizer.getVirtualItems().map(vItem => (
          <div
            key={vItem.key}
            style={{
              position: 'absolute',
              top: 0,
              transform: `translateY(${vItem.start}px)`,
            }}
          >
            {items[vItem.index].name}
          </div>
        ))}
      </div>
    </div>
  );
}
大小 特点
@tanstack/react-virtual ~5KB 无头库,灵活,支持动态尺寸
react-window ~6KB API 更简,适合固定高度
react-virtuoso ~9KB 最开箱即用,自动处理动态高度

React DevTools Profiler

使用步骤

  1. 打开 DevTools → Profiler 面板
  2. 点击录制按钮
  3. 操作应用(点击、输入等)
  4. 停止录制
  5. 分析火焰图和排名

关键指标

火焰图颜色含义:
- 灰色:组件未渲染(被跳过)
- 绿色 → 黄色 → 红色:渲染耗时由低到高

渲染次数(Commit):
- 每次状态更新 = 一次 Commit
- 关注高频 Commit(如连续输入触发十几次)

避免的常见模式

1. 在渲染中创建组件

// ❌ 每次渲染创建新组件 → React 视为完全不同类型 → 卸载+重新挂载
function Parent() {
  function SlowChild() { return <ExpensiveStuff />; }
  return <SlowChild />;
}

// ✅ 在外部定义或使用 useCallback
function SlowChild() { return <ExpensiveStuff />; }
function Parent() { return <SlowChild />; }

2. Context 值不当导致全局重渲染

// ❌ 每次渲染 value 都是新对象 → 所有消费者重渲染
<Context.Provider value={{ theme, user }}>
  {children}
</Context.Provider>

// ✅ 用 useMemo 稳定 value
const value = useMemo(() => ({ theme, user }), [theme, user]);
<Context.Provider value={value}>{children}</Context.Provider>

3. 滥用 React.memo

// ❌ 没必要:原生 HTML 元素不需要 memo
const MemoDiv = React.memo(({ children }) => <div>{children}</div>);

Web Vitals 指标

指标 含义 目标
FCP (First Contentful Paint) 首次有内容渲染 < 1.8s
LCP (Largest Contentful Paint) 最大内容完成渲染 < 2.5s
TBT (Total Blocking Time) 主线程阻塞总时长 < 200ms
CLS (Cumulative Layout Shift) 累计布局偏移 < 0.1
INP (Interaction to Next Paint) 交互延时 < 200ms
// 测量 Web Vitals
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(console.log);
onINP(console.log);
onCLS(console.log);

🚨 常见陷阱

  1. 过早优化:在确认性能问题前,memo/useMemo/useCallback 反而增加复杂度
  2. memo 配合内联对象/函数:传入新对象 → memo 形同虚设
  3. Context 大对象更新:所有消费者重渲染 → 拆分 Context 或用选择器方案
  4. 缺少 key:列表中没有 key 或 key 使用不稳定
  5. 渲染中创建新组件:每次都是"不同类型",强制卸载挂载
  6. 大列表不虚拟化:10000 条数据直接渲染 → 几百 ms 的白屏