正在加载,请稍候…

React 性能优化:memo、useMemo、useCallback 与虚拟化

识别并修复 React 性能瓶颈。学习何时使用 memo、useMemo、useCallback、React.lazy、react-virtual 虚拟化以及性

React 性能优化:memo、useMemo、useCallback 与虚拟化

React 性能优化

先进行性能分析

在优化之前始终先进行性能分析。使用 React DevTools Profiler 查找缓慢的渲染。

// 使用 Profiler 包裹以测量
import { Profiler } from 'react';

<Profiler
  id="UserList"
  onRender={(id, phase, actualDuration) => {
    if (actualDuration > 16) { // 慢于 60fps
      console.warn(`${id} took ${actualDuration}ms in ${phase}`);
    }
  }}
>
  <UserList />
</Profiler>

React 性能优化:memo、useMemo、useCallback 与虚拟化 插图

React.memo

当 props 未变化时阻止重新渲染。

// 不使用 memo:父组件渲染时总是重新渲染
function UserCard({ user }: { user: User }) {
  return <div>{user.name}</div>;
}

// 使用 memo:仅当 user prop 变化时重新渲染
const UserCard = React.memo(({ user }: { user: User }) => {
  return <div>{user.name}</div>;
});

// 自定义比较
const UserCard = React.memo(
  ({ user }: { user: User }) => <div>{user.name}</div>,
  (prev, next) => prev.user.id === next.user.id && prev.user.name === next.user.name
);

useMemo - 记忆化昂贵计算

function UserList({ users, searchQuery }: Props) {
  // 不使用 useMemo:每次渲染都进行过滤
  // const filtered = users.filter(u => u.name.includes(searchQuery));

  // 使用 useMemo:仅当 users 或 searchQuery 变化时重新计算
  const filtered = useMemo(
    () => users.filter(u => u.name.toLowerCase().includes(searchQuery.toLowerCase())),
    [users, searchQuery]
  );

  const stats = useMemo(() => ({
    total: users.length,
    active: users.filter(u => u.active).length,
    admins: users.filter(u => u.role === 'admin').length,
  }), [users]);

  return (
    <div>
      <Stats {...stats} />
      {filtered.map(user => <UserCard key={user.id} user={user} />)}
    </div>
  );
}

React 性能优化:memo、useMemo、useCallback 与虚拟化 插图

useCallback - 稳定的函数引用

function UserList() {
  const [users, setUsers] = useState<User[]>([]);

  // 不使用 useCallback:每次渲染都生成新的函数引用
  // -> 导致 UserCard 即使 users 未变化也重新渲染
  // const handleDelete = (id: string) => setUsers(u => u.filter(user => user.id !== id));

  // 使用 useCallback:稳定引用,UserCard 不会不必要地重新渲染
  const handleDelete = useCallback((id: string) => {
    setUsers(prev => prev.filter(user => user.id !== id));
  }, []); // 空依赖:函数永不变化

  return (
    <>
      {users.map(user => (
        <UserCard key={user.id} user={user} onDelete={handleDelete} />
      ))}
    </>
  );
}

使用 React.lazy 进行代码分割

import { lazy, Suspense } from 'react';

// 仅在需要时加载组件
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Analytics = lazy(() => import('./pages/Analytics'));
const Settings = lazy(() => import('./pages/Settings'));

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

React 性能优化:memo、useMemo、useCallback 与虚拟化 插图

长列表虚拟化

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

function VirtualUserList({ users }: { users: User[] }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: users.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 72, // 估计行高
    overscan: 5,
  });

  return (
    <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
      <div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
        {virtualizer.getVirtualItems().map(virtualItem => (
          <div
            key={virtualItem.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: `${virtualItem.size}px`,
              transform: `translateY(${virtualItem.start}px)`,
            }}
          >
            <UserCard user={users[virtualItem.index]} />
          </div>
        ))}
      </div>
    </div>
  );
}

使用 useTransition 处理非紧急更新

function SearchPage() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState<User[]>([]);
  const [isPending, startTransition] = useTransition();

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

    startTransition(() => {
      // 非紧急:如果用户仍在输入,可以延迟
      setResults(filterUsers(value));
    });
  }

  return (
    <div>
      <input value={query} onChange={e => handleSearch(e.target.value)} />
      {isPending ? <Spinner /> : <UserList users={results} />}
    </div>
  );
}

过早优化是万恶之源。先进行性能分析,然后优化正确的地方。