跳到主要内容

React 性能优化

本章将介绍 React 应用中常见的性能问题及其优化方法。性能优化的核心目标是:减少不必要的渲染次数、减小渲染压力、缓存计算结果。


性能优化思路

在开始优化之前,首先要明确性能优化的三个核心思路:

  1. 减少渲染次数:避免不必要的组件更新
  2. 减小渲染压力:将耗时任务拆分或延迟
  3. 缓存计算结果:避免重复执行昂贵的计算逻辑

优化应该在发现性能问题后进行,而不是过早优化。使用 React DevTools Profiler 来识别真正的性能瓶颈。


1. 避免不必要的重新渲染

React.memo

React.memo 是一个高阶组件,它会对组件的 props 进行浅比较。如果 props 没有变化,React 将跳过渲染该组件。

实时编辑器
const Child = React.memo(({ name }) => {
  console.log("子组件渲染了");
  return (
    <div style={{ 
      padding: '15px', 
      border: '2px solid #3b82f6', 
      borderRadius: '8px',
      background: '#eff6ff'
    }}>
      <strong>子组件:</strong>你好, {name}
    </div>
  );
});

function MemoDemo() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("张三");

  return (
    <div style={{ padding: '15px', background: '#f8fafc', borderRadius: '8px' }}>
      <p style={{ fontSize: '18px' }}>父组件计数: <strong>{count}</strong></p>
      <div style={{ display: 'flex', gap: '10px', marginBottom: '15px' }}>
        <button 
          onClick={() => setCount(c => c + 1)}
          style={{ padding: '8px 16px', background: '#6b7280', color: 'white', border: 'none', borderRadius: '4px' }}
        >
          增加计数(不触发子组件重绘)
        </button>
        <button 
          onClick={() => setName(n => n === "张三" ? "李四" : "张三")}
          style={{ padding: '8px 16px', background: '#3b82f6', color: 'white', border: 'none', borderRadius: '4px' }}
        >
          修改名称(触发子组件重绘)
        </button>
      </div>
      <Child name={name} />
      <p style={{ fontSize: '12px', color: '#64748b', marginTop: '10px' }}>
        打开控制台查看渲染日志
      </p>
    </div>
  );
}
结果
Loading...

何时使用 React.memo

不是所有组件都需要 React.memo,它本身也有开销。以下情况适合使用:

  • 组件渲染开销较大(复杂的 JSX 或计算)
  • 经常以相同的 props 重新渲染
  • 作为列表项渲染大量相同组件

自定义比较函数

当浅比较不足以判断 props 是否变化时,可以提供自定义比较函数:

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

const UserCard = React.memo(function UserCard({ user }) {
return <div>{user.name}</div>;
}, arePropsEqual);

2. 缓存计算与回调

useMemo:缓存计算结果

useMemo 用于缓存计算结果,避免每次渲染都重新计算。

实时编辑器
function UseMemoDemo() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  // 模拟昂贵的计算
  const expensiveValue = useMemo(() => {
    console.log("执行昂贵计算...");
    let result = 0;
    for (let i = 0; i < 1000000; i++) {
      result += i;
    }
    return result + count;
  }, [count]); // 仅在 count 变化时重新计算

  return (
    <div style={{ padding: '15px', background: '#f0fdf4', borderRadius: '8px' }}>
      <h4 style={{ margin: '0 0 12px 0', color: '#166534' }}>useMemo 演示</h4>
      <p style={{ fontSize: '24px', fontWeight: 'bold', color: '#15803d' }}>
        计算结果: {expensiveValue.toLocaleString()}
      </p>
      <div style={{ display: 'flex', gap: '10px', alignItems: 'center', marginTop: '12px' }}>
        <button 
          onClick={() => setCount(c => c + 1)}
          style={{ padding: '8px 16px', background: '#22c55e', color: 'white', border: 'none', borderRadius: '4px' }}
        >
          更新计数(触发重新计算)
        </button>
        <input 
          value={text} 
          onChange={(e) => setText(e.target.value)} 
          placeholder="输入内容不触发昂贵计算" 
          style={{ padding: '8px', border: '1px solid #d1d5db', borderRadius: '4px', flex: 1 }}
        />
      </div>
      <p style={{ fontSize: '12px', color: '#64748b', marginTop: '8px' }}>
        打开控制台查看计算日志
      </p>
    </div>
  );
}
结果
Loading...

useCallback:缓存函数引用

useCallback 用于缓存函数定义,避免每次渲染创建新函数。这在与 React.memo 配合使用时特别重要。

实时编辑器
const Button = React.memo(({ onClick, label }) => {
  console.log('Button 渲染:', label);
  return (
    <button 
      onClick={onClick}
      style={{ padding: '8px 16px', margin: '4px' }}
    >
      {label}
    </button>
  );
});

function UseCallbackDemo() {
  const [count, setCount] = useState(0);
  const [items, setItems] = useState([]);

  // 不使用 useCallback,每次渲染都会创建新函数
  // 使用 useCallback 缓存函数引用
  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []);

  const handleAddItem = useCallback(() => {
    setItems(prev => [...prev, `项目 ${prev.length + 1}`]);
  }, []);

  return (
    <div style={{ padding: '15px', background: '#fef3c7', borderRadius: '8px' }}>
      <h4 style={{ margin: '0 0 12px 0' }}>useCallback 演示</h4>
      <p>计数: {count}</p>
      <div style={{ marginBottom: '12px' }}>
        <Button onClick={handleClick} label="增加计数" />
        <Button onClick={handleAddItem} label="添加项目" />
      </div>
      <ul style={{ margin: 0, padding: 0, listStyle: 'none' }}>
        {items.map((item, i) => (
          <li key={i} style={{ padding: '4px', background: '#fef9c3', marginBottom: '4px', borderRadius: '4px' }}>
            {item}
          </li>
        ))}
      </ul>
    </div>
  );
}
结果
Loading...

useMemo vs useCallback

  • useMemo:缓存任何值的计算结果
  • useCallback:专门缓存函数,等价于 useMemo(() => fn, deps)
// 这两种写法等价
const memoizedCallback = useCallback(() => doSomething(a, b), [a, b]);
const memoizedCallback = useMemo(() => () => doSomething(a, b), [a, b]);

3. 列表优化

使用虚拟列表

当渲染包含数千个条目的列表时,只渲染可见区域的 DOM 节点可以大幅提升性能。推荐使用 react-window@tanstack/react-virtual

Key 的正确使用

正确使用 key 可以让 React 更高效地更新列表:

实时编辑器
function KeyDemo() {
  const [items, setItems] = useState([
    { id: 'a1', text: '第一条项目' },
    { id: 'b2', text: '第二条项目' },
    { id: 'c3', text: '第三条项目' }
  ]);
  
  const addItem = () => {
    const newId = String(Date.now());
    setItems(prev => [{ id: newId, text: `新项目 ${newId}` }, ...prev]);
  };
  
  const reverseItems = () => {
    setItems(prev => [...prev].reverse());
  };
  
  return (
    <div style={{ padding: '15px' }}>
      <div style={{ marginBottom: '12px', display: 'flex', gap: '8px' }}>
        <button onClick={addItem} style={{ padding: '6px 12px' }}>添加项目</button>
        <button onClick={reverseItems} style={{ padding: '6px 12px' }}>反转列表</button>
      </div>
      <ul style={{ listStyle: 'none', padding: 0 }}>
        {items.map(item => (
          <li 
            key={item.id} 
            style={{ 
              padding: '10px', 
              background: '#f8fafc', 
              marginBottom: '4px', 
              borderRadius: '4px',
              borderLeft: '4px solid #3b82f6'
            }}
          >
            <code style={{ color: '#6b7280' }}>key: {item.id}</code>
            <span style={{ marginLeft: '10px' }}>{item.text}</span>
          </li>
        ))}
      </ul>
    </div>
  );
}
结果
Loading...

Key 使用规则

  • 使用稳定的、唯一的 ID,不要用数组索引
  • 不要在渲染时动态生成 key(如 Math.random()
  • Key 只需要在兄弟节点中唯一

4. 并发渲染(React 18+)

React 18 引入了并发特性,允许将更新标记为不同的优先级。

useTransition

useTransition 用于将紧急更新(如输入文字)与非紧急更新(如过滤列表)分离,保持 UI 响应。

实时编辑器
function TransitionDemo() {
  const [query, setQuery] = useState("");
  const [list, setList] = useState([]);
  const [isPending, startTransition] = useTransition();

  const handleChange = (e) => {
    const value = e.target.value;
    setQuery(value); // 紧急更新:立即反映输入

    // 非紧急更新:在后台处理列表过滤
    startTransition(() => {
      const items = Array.from({ length: 5000 }, (_, i) => `${value} 项目 ${i + 1}`);
      setList(items);
    });
  };

  return (
    <div style={{ padding: '15px', background: '#faf5ff', borderRadius: '8px' }}>
      <h4 style={{ margin: '0 0 12px 0', color: '#7c3aed' }}>useTransition 演示</h4>
      <input 
        value={query} 
        onChange={handleChange} 
        placeholder="键入文字查看并发渲染..." 
        style={{ 
          width: '100%', 
          padding: '10px', 
          border: '1px solid #d1d5db', 
          borderRadius: '4px',
          marginBottom: '12px',
          fontSize: '16px'
        }}
      />
      {isPending && (
        <p style={{ color: '#7c3aed', fontStyle: 'italic' }}>正在计算过滤结果...</p>
      )}
      <ul style={{ 
        maxHeight: '150px', 
        overflow: 'auto', 
        listStyle: 'none', 
        padding: 0,
        background: 'white',
        borderRadius: '4px',
        border: '1px solid #e2e8f0'
      }}>
        {list.slice(0, 50).map((item, i) => (
          <li key={i} style={{ padding: '6px 10px', borderBottom: '1px solid #f1f5f9' }}>
            {item}
          </li>
        ))}
        {list.length > 50 && (
          <li style={{ padding: '6px 10px', color: '#64748b', textAlign: 'center' }}>
            ... 还有 {list.length - 50}
          </li>
        )}
      </ul>
    </div>
  );
}
结果
Loading...

useDeferredValue

useDeferredValue 让你可以延迟更新某个值,适用于无法控制更新源的场景:

function SearchResults({ query }) {
// 延迟查询值,让输入保持响应
const deferredQuery = useDeferredValue(query);

return (
<div>
{deferredQuery && <ResultList query={deferredQuery} />}
</div>
);
}

5. 代码分割

React.lazy 和 Suspense

通过动态导入组件,减少首屏加载的包体积:

import { lazy, Suspense } from 'react';

// 懒加载组件
const HeavyComponent = lazy(() => import('./HeavyComponent'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<HeavyComponent />
</Suspense>
);
}

路由级代码分割

最常见的代码分割场景是与路由配合:

import { BrowserRouter, Routes, Route } from 'react-router-dom';

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

function App() {
return (
<BrowserRouter>
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}

预加载

可以在用户可能需要的组件之前预加载:

// 鼠标悬停时预加载
const handleMouseEnter = () => {
import('./HeavyComponent');
};

<button onMouseEnter={handleMouseEnter}>
加载重型组件
</button>

6. 其他优化技巧

防抖和节流

对于高频触发的事件,使用防抖或节流:

实时编辑器
function DebounceDemo() {
  const [searchTerm, setSearchTerm] = useState('');
  const [debouncedTerm, setDebouncedTerm] = useState('');
  const [results, setResults] = useState([]);

  // 防抖函数
  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedTerm(searchTerm);
    }, 500);

    return () => clearTimeout(timer);
  }, [searchTerm]);

  // 模拟搜索
  useEffect(() => {
    if (debouncedTerm) {
      setResults([
        `${debouncedTerm} - 结果 1`,
        `${debouncedTerm} - 结果 2`,
        `${debouncedTerm} - 结果 3`
      ]);
    }
  }, [debouncedTerm]);

  return (
    <div style={{ padding: '15px', background: '#ecfdf5', borderRadius: '8px' }}>
      <h4 style={{ margin: '0 0 12px 0', color: '#059669' }}>防抖演示</h4>
      <input
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="输入搜索内容(500ms 防抖)"
        style={{ width: '100%', padding: '10px', border: '1px solid #d1d5db', borderRadius: '4px' }}
      />
      <p style={{ fontSize: '14px', color: '#6b7280' }}>
        输入值: {searchTerm} | 防抖值: {debouncedTerm}
      </p>
      {results.length > 0 && (
        <ul style={{ margin: 0, padding: '0 0 0 20px' }}>
          {results.map((r, i) => <li key={i}>{r}</li>)}
        </ul>
      )}
    </div>
  );
}
结果
Loading...

避免内联对象和函数

内联对象和函数在每次渲染时都会创建新引用,可能导致子组件不必要的重渲染:

// 避免:每次渲染都创建新对象
<Button style={{ color: 'red' }} onClick={() => doSomething()} />

// 推荐:提取到组件外部或使用 useMemo/useCallback
const buttonStyle = { color: 'red' };
const handleClick = useCallback(() => doSomething(), []);

<Button style={buttonStyle} onClick={handleClick} />

使用 Fragment 减少嵌套

减少不必要的 DOM 节点:

// 不好的做法:添加不必要的 div
function List() {
return (
<div>
<h1>标题</h1>
<ul>{items}</ul>
</div>
);
}

// 好的做法:使用 Fragment
function List() {
return (
<>
<h1>标题</h1>
<ul>{items}</ul>
</>
);
}

常见性能问题及解决方案

问题症状解决方案
不必要的重新渲染组件频繁渲染React.memo、useMemo、useCallback
大列表渲染慢滚动卡顿虚拟列表、分页、懒加载
重复计算CPU 占用高useMemo
重复创建函数子组件重渲染useCallback
大 Bundle首屏加载慢代码分割、懒加载
输入延迟打字卡顿useTransition、防抖
内存泄漏内存持续增长清理 useEffect、取消订阅

性能检查清单

优化前,使用 React DevTools Profiler 识别真正的瓶颈。以下是一个检查清单:

渲染优化

  • 使用 React DevTools Profiler 分析
  • 检查不必要的重新渲染
  • 使用 React.memo 包装纯组件
  • 使用 useMemo 缓存计算结果
  • 使用 useCallback 缓存回调函数

列表优化

  • 使用正确的 key
  • 考虑虚拟列表
  • 实现分页或懒加载

加载优化

  • 实现代码分割
  • 使用 React.lazy 懒加载
  • 预加载关键资源

其他

  • 清理副作用和取消订阅
  • 生产环境构建
  • 监控性能指标

小结

本章我们学习了 React 性能优化的核心方法:

避免不必要的渲染

  • React.memo:跳过 props 未变化的组件渲染
  • useMemo:缓存昂贵的计算结果
  • useCallback:缓存函数引用

列表优化

  • 使用稳定的 key
  • 虚拟列表处理大数据

并发渲染

  • useTransition:分离紧急和非紧急更新
  • useDeferredValue:延迟更新某些值

代码分割

  • React.lazy:懒加载组件
  • 配合路由实现页面级分割

最佳实践

  • 使用 React DevTools Profiler 识别瓶颈
  • 避免过早优化
  • 在正确的地方使用正确的优化技术

练习

  1. 创建一个列表组件,渲染 1000 条数据,比较使用 React.memo 前后的性能差异
  2. 实现一个搜索组件,使用 useTransition 优化大列表过滤
  3. 使用 useMemo 优化一个复杂的数据转换组件
  4. 实现一个带防抖的搜索输入框
  5. 使用 React DevTools Profiler 分析你的应用,找出性能瓶颈

参考资源