正在加载,请稍候…

React Performance Optimization: memo, useMemo, useCallback, and Virtualization

Identify and fix React performance bottlenecks. Learn when to use memo, useMemo, useCallback, React.lazy, virtualization with react-virtual, and profiling tools.

React Performance Optimization

Profiling First

Always profile before optimizing. Use React DevTools Profiler to find slow renders.

// Wrap with Profiler to measure
import { Profiler } from 'react';

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

React.memo

Prevents re-renders when props haven't changed.

// Without memo: re-renders whenever parent renders
function UserCard({ user }: { user: User }) {
  return <div>{user.name}</div>;
}

// With memo: only re-renders when user prop changes
const UserCard = React.memo(({ user }: { user: User }) => {
  return <div>{user.name}</div>;
});

// Custom comparison
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 - Memoize Expensive Computations

function UserList({ users, searchQuery }: Props) {
  // Without useMemo: filters on every render
  // const filtered = users.filter(u => u.name.includes(searchQuery));

  // With useMemo: only recomputes when users or searchQuery changes
  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>
  );
}

useCallback - Stable Function References

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

  // Without useCallback: new function reference on every render
  // -> causes UserCard to re-render even when users haven't changed
  // const handleDelete = (id: string) => setUsers(u => u.filter(user => user.id !== id));

  // With useCallback: stable reference, UserCard doesn't re-render unnecessarily
  const handleDelete = useCallback((id: string) => {
    setUsers(prev => prev.filter(user => user.id !== id));
  }, []); // empty deps: function never changes

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

Code Splitting with React.lazy

import { lazy, Suspense } from 'react';

// Load components only when needed
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>
  );
}

Virtualization for Long Lists

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, // estimated row height
    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 for Non-Urgent Updates

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

  function handleSearch(value: string) {
    setQuery(value); // Urgent: update input immediately

    startTransition(() => {
      // Non-urgent: can defer if user is still typing
      setResults(filterUsers(value));
    });
  }

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

Premature optimization is the root of all evil. Profile first, then optimize the right things.