学习路径 高级

性能优化最佳实践

学习如何优化 React 应用的性能,提升用户体验

概述

React 应用性能优化的核心思想是:减少不必要的渲染和计算。通过合理使用 React 提供的工具和最佳实践,可以让应用运行得更快、更流畅。

性能优化的重要性

  • 更好的用户体验:页面响应更快,交互更流畅
  • 更低的资源消耗:减少 CPU 和内存使用
  • 更好的移动端体验:电池寿命更长,流量消耗更少
  • 更高的 SEO 评分:页面加载速度影响搜索排名
过早优化是万恶之源

在优化之前,先确认性能瓶颈在哪里。使用 React DevTools Profiler 和浏览器性能分析工具来识别真正需要优化的地方。


1. 使用 React.memo 避免不必要的重渲染

问题场景

当父组件渲染时,所有子组件都会重新渲染,即使 props 没有变化:

TypeScript
function ExpensiveComponent({ data }: { data: UserData }) {
  console.log('ExpensiveComponent 渲染');
  // 复杂的计算或渲染逻辑
  return <div>{data.name}</div>;
}

function Parent() {
  const [count, setCount] = useState(0);
  const [data] = useState({ name: 'Taylor' });

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        点击次数: {count}
      </button>
      {/* count 改变时,ExpensiveComponent 也会重新渲染 */}
      <ExpensiveComponent data={data} />
    </div>
  );
}

解决方案:React.memo

TypeScript
import { memo } from 'react';

// 使用 memo 包装组件
const ExpensiveComponent = memo(function ExpensiveComponent(
  { data }: { data: UserData }
) {
  console.log('ExpensiveComponent 渲染');
  return <div>{data.name}</div>;
});

// 现在只有 data 变化时才会重新渲染

自定义比较函数

TypeScript
const ExpensiveComponent = memo(
  function ExpensiveComponent({ data }: { data: UserData }) {
    return <div>{data.name}</div>;
  },
  // 自定义比较函数
  (prevProps, nextProps) => {
    // 返回 true 表示 props 相等,不需要重新渲染
    return prevProps.data.id === nextProps.data.id;
  }
);
常见错误

不要在默认比较函数中使用内联对象或数组:

// ❌ 错误:每次都是新对象
<ExpensiveComponent data={{ name: 'Taylor' }} />

// ✅ 正确:使用 useMemo 或移到外部
const data = { name: 'Taylor' };
<ExpensiveComponent data={data} />

2. 使用 useMemo 缓存计算结果

问题场景

每次渲染都执行昂贵的计算:

TypeScript
function ProductList({ products }: { products: Product[] }) {
  // 每次渲染都会重新计算
  const sortedProducts = products.sort((a, b) =>
    a.price - b.price
  );

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

解决方案:useMemo

TypeScript
import { useMemo } from 'react';

function ProductList({ products }: { products: Product[] }) {
  // 只在 products 变化时重新计算
  const sortedProducts = useMemo(
    () => products.sort((a, b) => a.price - b.price),
    [products] // 依赖数组
  );

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

适用场景

TypeScript
// ✅ 适合:昂贵的计算
const filteredData = useMemo(
  () => data.filter(item => item.isActive),
  [data]
);

// ✅ 适合:复杂对象
const formData = useMemo(
  () => ({
    username,
    email,
    preferences: userPreferences,
  }),
  [username, email, userPreferences]
);

// ❌ 不适合:简单计算
const doubled = useMemo(() => count * 2, [count]);
// 直接写:const doubled = count * 2;

3. 使用 useCallback 缓存函数

问题场景

每次渲染都创建新的函数引用,导致子组件不必要的重渲染:

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

  // 每次渲染都是新函数
  const handleClick = () => {
    console.log('Clicked');
  };

  return <Child onClick={handleClick} />;
}

const Child = memo(function Child({
  onClick
}: {
  onClick: () => void;
}) {
  console.log('Child 渲染');
  return <button onClick={onClick}>点击</button>;
});

解决方案:useCallback

TypeScript
import { useCallback } from 'react';

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

  // 函数引用保持稳定
  const handleClick = useCallback(() => {
    console.log('Clicked');
  }, []); // 依赖数组为空,函数永不改变

  return <Child onClick={handleClick} />;
}

带依赖的 useCallback

TypeScript
function ProductList({ products }: { products: Product[] }) {
  const [filter, setFilter] = useState('');

  // filter 变化时才创建新函数
  const filteredProducts = useCallback(
    () => products.filter(p => p.name.includes(filter)),
    [products, filter]
  );

  return (
    <div>
      <input
        value={filter}
        onChange={e => setFilter(e.target.value)}
      />
      <ProductList products={filteredProducts()} />
    </div>
  );
}
useCallback 和 useMemo 的关系

useCallback(fn, deps) 等价于 useMemo(() => fn, deps)。 useCallback 是专门用于缓存函数的语法糖。


4. 虚拟化长列表

问题场景

渲染大量 DOM 元素会导致性能问题:

TypeScript
// ❌ 渲染 10000 个元素
function BigList({ items }: { items: Item[] }) {
  return (
    <div>
      {items.map(item => (
        <div key={item.id}>
          {item.name}
        </div>
      ))}
    </div>
  );
}

解决方案:虚拟滚动

使用专业的虚拟滚动库,只渲染可见区域的元素:

TypeScript
// 使用 react-window
import { FixedSizeList } from 'react-window';

function VirtualList({ items }: { items: Item[] }) {
  const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
    <div style={style}>
      {items[index].name}
    </div>
  );

  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={50}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}
1

安装虚拟滚动库:npm install react-window
2

使用 FixedSizeList(固定高度)或 VariableSizeList(可变高度)
3

只渲染可见区域的项目,大幅提升性能
4

适用于超过 1000 个元素的列表

5. 代码分割和懒加载

使用 React.lazy 懒加载组件

TypeScript
import { lazy, Suspense } from 'react';

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

function App() {
  return (
    <Suspense fallback={<div>加载中...</div>}>
      <HeavyComponent />
    </Suspense>
  );
}

路由级别的代码分割

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

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

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<div>加载中...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/dashboard" element={<Dashboard />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

Next.js 自动代码分割

TypeScript
// Next.js 会自动为每个页面进行代码分割
// pages/dashboard.tsx 会自动分割成独立的 chunk

export default function Dashboard() {
  return <div>Dashboard</div>;
}

6. 避免不必要的匿名函数

问题场景

在 JSX 中创建匿名函数会导致子组件不必要的重渲染:

TypeScript
// ❌ 每次渲染都是新函数
function List() {
  return (
    <div>
      {items.map(item => (
        <button
          key={item.id}
          onClick={() => handleClick(item.id)} // 新函数
        >
          {item.name}
        </button>
      ))}
    </div>
  );
}

解决方案 1:提取函数

TypeScript
// ✅ 提取函数
function List() {
  const handleItemClick = (id: string) => {
    handleClick(id);
  };

  return (
    <div>
      {items.map(item => (
        <button
          key={item.id}
          onClick={() => handleItemClick(item.id)}
        >
          {item.name}
        </button>
      ))}
    </div>
  );
}

解决方案 2:使用 data 属性

TypeScript
// ✅ 使用 data 属性
function List() {
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    const id = e.currentTarget.dataset.id;
    handleClick(id);
  };

  return (
    <div>
      {items.map(item => (
        <button
          key={item.id}
          data-id={item.id}
          onClick={handleClick}
        >
          {item.name}
        </button>
      ))}
    </div>
  );
}

7. 优化 State 结构

避免冗余 State

TypeScript
// ❌ 冗余的 state
function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [fullName, setFullName] = useState('');

  const updateFirstName = (name: string) => {
    setFirstName(name);
    setFullName(`${name} ${lastName}`); // 需要同步更新
  };

  // ...
}

// ✅ 派生 state
function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  // fullName 可以从 firstName 和 lastName 派生
  const fullName = `${firstName} ${lastName}`;

  // ...
}

合并相关 State

TypeScript
// ❌ 分散的 state
function Profile() {
  const [name, setName] = useState('');
  const [age, setAge] = useState(0);
  const [email, setEmail] = useState('');

  // ...
}

// ✅ 合并为对象
function Profile() {
  const [user, setUser] = useState({
    name: '',
    age: 0,
    email: '',
  });

  const updateField = (field: string, value: string | number) => {
    setUser(prev => ({ ...prev, [field]: value }));
  };

  // ...
}
何时使用 useReducer

当 state 逻辑复杂且涉及多个子值,或者下一个 state 依赖于之前的 state 时,考虑使用 useReducer


8. 使用 key 优化列表渲染

正确使用 key

TypeScript
// ❌ 使用索引作为 key(只在静态列表时可以)
function List({ items }: { items: Item[] }) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{item.name}</li>
      ))}
    </ul>
  );
}

// ✅ 使用稳定的唯一标识符
function List({ items }: { items: Item[] }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

key 的作用

为什么 key 很重要?

key 帮助 React 识别哪些元素改变了、添加了或删除了。 正确的 key 可以让 React 复用 DOM 元素,避免不必要的操作。

  • 使用稳定的、唯一的、可预测的值
  • 不要使用索引(如果列表会重新排序)
  • 不要使用随机数或时间戳

9. 使用 Transition 优先级更新

标记非紧急更新

TypeScript
import { useState, useTransition } from 'react';

function Search() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;

    // 紧急更新:立即更新输入框
    setQuery(value);

    // 非紧急更新:搜索结果可以稍后
    startTransition(() => {
      setSearchResults(filterResults(value));
    });
  };

  return (
    <div>
      <input value={query} onChange={handleChange} />
      {isPending && <div>搜索中...</div>}
      <Results items={searchResults} />
    </div>
  );
}
Transition 使用场景
  • 搜索框输入
  • 过滤大型列表
  • 页面导航
  • 任何可以延迟的非阻塞 UI 更新

10. 避免在渲染中创建对象

问题场景

TypeScript
// ❌ 每次渲染都创建新对象
function Button() {
  return (
    <button
      style={{ color: 'red', fontSize: '16px' }} // 新对象
    >
      点击
    </button>
  );
}

解决方案

TypeScript
// ✅ 移到组件外部
const buttonStyle = { color: 'red', fontSize: '16px' };

function Button() {
  return <button style={buttonStyle}>点击</button>;
}

// 或使用 useMemo
function Button() {
  const style = useMemo(
    () => ({ color: 'red', fontSize: '16px' }),
    []
  );

  return <button style={style}>点击</button>;
}

性能检测工具

React DevTools Profiler

  1. 安装 React DevTools 浏览器扩展
  2. 打开 Profiler 标签
  3. 点击录制
  4. 与应用交互
  5. 停止录制并分析

使用示例

TypeScript
// 标记组件用于 Profiler
import { Profiler } from 'react';

function onRenderCallback(
  id: string,
  phase: 'mount' | 'update',
  actualDuration: number,
  baseDuration: number,
  startTime: number,
  commitTime: number
) {
  console.log({
    id,
    phase,
    actualDuration,
    baseDuration,
  });
}

function App() {
  return (
    <Profiler id="App" onRender={onRenderCallback}>
      <Navigation />
      <MainContent />
    </Profiler>
  );
}

检查清单

在优化性能时,按以下顺序检查:

1

使用 Profiler 识别性能瓶颈
2

避免不必要的重渲染(React.memo)
3

缓存昂贵的计算(useMemo)
4

缓存函数引用(useCallback)
5

虚拟化长列表
6

代码分割和懒加载
7

优化 state 结构
8

使用 transition 优化非紧急更新
记住

过早优化是万恶之源。只优化真正影响用户体验的 性能问题。大多数情况下,React 默认的行为已经足够好了。


相关资源