正在加载,请稍候…

React 性能优化:实用清单

通过 React.memo、useMemo、useCallback、代码分割、虚拟化及避免常见重渲染陷阱等成熟技术,加速你的 React 应用。

React 性能优化:实用清单

React 应用为何变慢

React 默认很快,但某些模式会导致不必要的工作:

  1. 不必要的重渲染 — 组件在输出不会改变时重新渲染
  2. 每次渲染都进行昂贵计算 — 不必要地重新计算派生数据
  3. 包体积过大 — 加载用户尚未需要的代码
  4. 长列表渲染 — 一次性渲染数千个 DOM 节点
  5. 布局抖动 — JS 读写操作迫使浏览器反复重新计算布局

本指南是一份实用清单,而非理论。

React 性能优化:实用清单 插图

第一步:先测量再优化

切勿盲目优化。首先使用这些工具:

// React DevTools Profiler
// 1. 安装 React DevTools 浏览器扩展
// 2. 打开 DevTools → Profiler 标签
// 3. 记录用户交互
// 4. 查找频繁渲染或耗时长的组件

// 在开发环境中检测不必要的重渲染
// 将此代码添加到组件以查看其渲染时机:
const MyComponent = ({ value }) => {
  console.log('MyComponent rendered:', value);
  return <div>{value}</div>;
};

// 或使用 react-why-did-you-render
import whyDidYouRender from '@welldone-software/why-did-you-render';
whyDidYouRender(React, { trackAllPureComponents: true });

第二步:修复不必要的重渲染

React.memo — 记忆化组件

// 不使用 memo:父组件重渲染时子组件总是重渲染
const UserCard = ({ user }) => (
  <div>{user.name} - {user.email}</div>
);

// 使用 memo:仅当 user prop 变化时重渲染(浅比较)
const UserCard = React.memo(({ user }) => (
  <div>{user.name} - {user.email}</div>
));

// 自定义比较(当浅比较不够用时)
const UserCard = React.memo(
  ({ user }) => <div>{user.name}</div>,
  (prevProps, nextProps) => prevProps.user.id === nextProps.user.id
);

何时使用: 频繁渲染但接收相同 props 的组件——列表项、图表节点、表格行。

何时不使用: 很少渲染的简单组件。记忆化开销可能比重渲染成本更高。

稳定的回调引用

// ❌ 每次渲染创建新函数 → 记忆化子组件总是重渲染
function ParentList() {
  const [items, setItems] = useState([]);

  return items.map(item => (
    <MemoizedItem
      key={item.id}
      item={item}
      onDelete={() => setItems(prev => prev.filter(i => i.id !== item.id))} // 每次渲染新函数
    />
  ));
}

// ✅ 使用 useCallback 保持稳定回调
function ParentList() {
  const [items, setItems] = useState([]);

  const handleDelete = useCallback((id) => {
    setItems(prev => prev.filter(i => i.id !== id));
  }, []); // 稳定引用

  return items.map(item => (
    <MemoizedItem key={item.id} item={item} onDelete={handleDelete} />
  ));
}

稳定的对象引用

// ❌ 每次渲染创建新对象
function Chart({ data }) {
  const options = { color: 'blue', animated: true }; // 每次渲染新引用

  return <MemoizedChart data={data} options={options} />; // 总是重渲染
}

// ✅ 记忆化 options 对象
function Chart({ data }) {
  const options = useMemo(
    () => ({ color: 'blue', animated: true }),
    [] // 永不改变——也可以作为模块级常量
  );

  return <MemoizedChart data={data} options={options} />;
}

React 性能优化:实用清单 插图

第三步:记忆化昂贵计算

// ❌ 每次渲染过滤 10,000 个项目
function ProductList({ products, search, category, maxPrice }) {
  const filtered = products
    .filter(p => p.name.toLowerCase().includes(search.toLowerCase()))
    .filter(p => category ? p.category === category : true)
    .filter(p => p.price <= maxPrice)
    .sort((a, b) => a.price - b.price);

  return filtered.map(p => <ProductCard key={p.id} product={p} />);
}

// ✅ 仅在输入变化时重新计算
function ProductList({ products, search, category, maxPrice }) {
  const filtered = useMemo(
    () => products
      .filter(p => p.name.toLowerCase().includes(search.toLowerCase()))
      .filter(p => category ? p.category === category : true)
      .filter(p => p.price <= maxPrice)
      .sort((a, b) => a.price - b.price),
    [products, search, category, maxPrice]
  );

  return filtered.map(p => <ProductCard key={p.id} product={p} />);
}

第四步:状态架构

就近放置状态

将状态尽可能靠近其使用位置。全局状态会导致广泛的重渲染。

// ❌ 工具提示打开状态放在全局 store → 每次悬停所有组件重渲染
const { isTooltipOpen, setTooltipOpen } = useGlobalStore();

// ✅ 工具提示状态局部于工具提示组件
function TooltipWrapper({ children, content }) {
  const [isOpen, setIsOpen] = useState(false); // 仅此组件重渲染
  return (
    <div onMouseEnter={() => setIsOpen(true)} onMouseLeave={() => setIsOpen(false)}>
      {children}
      {isOpen && <Tooltip>{content}</Tooltip>}
    </div>
  );
}

拆分 Context

// ❌ 一个 context 包含所有内容——任何更新都会重渲染所有消费者
const AppContext = createContext({ user, theme, cart, notifications });

// ✅ 拆分为独立的 context
const UserContext = createContext(user);
const ThemeContext = createContext(theme);
const CartContext = createContext(cart);

// 组件只订阅所需内容
function PriceTag() {
  const cart = useContext(CartContext); // 仅在 cart 变化时重渲染
  return <span>{cart.total}</span>;
}

第五步:列表虚拟化

渲染 1000+ 行会创建 1000+ 个 DOM 节点。虚拟化只渲染可见部分。

import { FixedSizeList as List } from 'react-window';

function VirtualizedList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style} className="list-item">
      {items[index].name}
    </div>
  );

  return (
    <List
      height={600}           // 可见容器高度
      itemCount={items.length}
      itemSize={50}          // 每个项目高度
      width="100%"
    >
      {Row}
    </List>
  );
}

// 对于可变高度项目
import { VariableSizeList } from 'react-window';

对于简单列表使用 react-window,对于更复杂的情况(包括网格和瀑布流布局)使用 react-virtual(TanStack Virtual)。

React 性能优化:实用清单 插图

第六步:代码分割

// ❌ 所有内容在一个包中
import HeavyEditor from './HeavyEditor';
import Chart from './Chart';

// ✅ 动态导入——仅在需要时加载
import { lazy, Suspense } from 'react';

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

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <Routes>
        <Route path="/editor" element={<HeavyEditor />} />
        <Route path="/analytics" element={<Chart />} />
      </Routes>
    </Suspense>
  );
}

悬停时预取

// 在用户点击前预加载代码块
function NavLink({ to, children }) {
  const prefetch = () => import('./HeavyPage'); // 触发代码块下载

  return (
    <Link to={to} onMouseEnter={prefetch} onFocus={prefetch}>
      {children}
    </Link>
  );
}

第七步:图片与资源优化

// 懒加载折叠线以下的图片
function ProductImage({ src, alt }) {
  return (
    <img
      src={src}
      alt={alt}
      loading="lazy"          // 原生懒加载
      decoding="async"        // 解码时不阻塞渲染
      width={300}
      height={200}            // 防止布局偏移
    />
  );
}

// 对于 Next.js — 使用 next/image
import Image from 'next/image';
<Image src={src} alt={alt} width={300} height={200} priority={false} />

第八步:React 18 并发特性

import { useTransition, useDeferredValue } from 'react';

// useTransition:将昂贵的状态更新标记为非紧急
function SearchBox({ items }) {
  const [query, setQuery] = useState('');
  const [filteredItems, setFilteredItems] = useState(items);
  const [isPending, startTransition] = useTransition();

  function handleSearch(e) {
    setQuery(e.target.value); // 紧急:立即更新输入

    startTransition(() => {
      // 非紧急:如果用户再次输入可中断
      setFilteredItems(items.filter(i => i.includes(e.target.value)));
    });
  }

  return (
    <>
      <input value={query} onChange={handleSearch} />
      {isPending && <Spinner />}
      <List items={filteredItems} />
    </>
  );
}

// useDeferredValue:延迟派生值
function SearchResults({ query, items }) {
  const deferredQuery = useDeferredValue(query); // 快速输入时滞后

  const filtered = useMemo(
    () => items.filter(i => i.includes(deferredQuery)),
    [items, deferredQuery]
  );

  return <List items={filtered} />;
}

性能清单

  • 先分析——确定实际瓶颈
  • 对频繁接收稳定 props 的组件使用 memo
  • 对传递给记忆化子组件的回调使用 useCallback
  • 对昂贵的过滤/排序列表使用 useMemo
  • 就近放置状态——避免将临时 UI 状态放在全局
  • 拆分 context——分离关注点
  • 虚拟化长列表(1000+ 项)
  • 对路由和重型组件进行代码分割
  • 使用 loading="lazy" 懒加载图片
  • 对慢状态更新使用 useTransition(React 18+)

→ 使用 Benchmark Builder 进行基准测试并测量性能指标。