RE

Performance

React · 6 entries

React.memo for Component Memoization

syntax
const Memoized = React.memo(Component);
// Re-renders only when props change (shallow comparison)
example
import { memo } from 'react';

const ChatMessage = memo(function ChatMessage({ author, text, timestamp }) {
  // This only re-renders when author, text, or timestamp change
  return (
    <div className="message">
      <strong>{author}</strong>
      <p>{text}</p>
      <time>{new Date(timestamp).toLocaleTimeString()}</time>
    </div>
  );
});

// Parent re-renders frequently (new messages arrive)
// but existing ChatMessage components skip re-rendering
function ChatFeed({ messages }) {
  return (
    <div>
      {messages.map(msg => (
        <ChatMessage
          key={msg.id}
          author={msg.author}
          text={msg.text}
          timestamp={msg.timestamp}
        />
      ))}
    </div>
  );
}

Note React.memo only does a shallow prop comparison. If you pass objects or functions that are recreated each render, memo has no effect. Pair with useMemo/useCallback for those props.

Code Splitting by Route

syntax
const Page = lazy(() => import('./Page'));
<Suspense fallback={<Spinner />}>
  <Page />
</Suspense>
example
import { lazy, Suspense } from 'react';

const Home = lazy(() => import('./pages/Home'));
const Products = lazy(() => import('./pages/Products'));
const Account = lazy(() => import('./pages/Account'));

function AppRouter() {
  const [route, setRoute] = useState('home');

  return (
    <nav>
      <button onClick={() => setRoute('home')}>Home</button>
      <button onClick={() => setRoute('products')}>Products</button>
      <button onClick={() => setRoute('account')}>Account</button>

      <Suspense fallback={<div className="page-loader">Loading page...</div>}>
        {route === 'home' && <Home />}
        {route === 'products' && <Products />}
        {route === 'account' && <Account />}
      </Suspense>
    </nav>
  );
}

Note Route-based code splitting gives the biggest bundle size reduction because entire page components and their dependencies are loaded on demand. Place the Suspense boundary around the lazy components, not the entire app.

Avoiding Unnecessary Renders

syntax
// 1. Move state down (closer to where it is used)
// 2. Lift content up (pass components as children)
// 3. Memoize with React.memo + useCallback/useMemo
example
// PROBLEM: typing in input re-renders the expensive list
function BadPage() {
  const [query, setQuery] = useState('');
  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <ExpensiveList /> {/* re-renders on every keystroke */}
    </div>
  );
}

// FIX 1: Move state down into its own component
function GoodPage() {
  return (
    <div>
      <SearchInput /> {/* state lives here now */}
      <ExpensiveList /> {/* no longer re-renders on keystrokes */}
    </div>
  );
}

function SearchInput() {
  const [query, setQuery] = useState('');
  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

// FIX 2: Lift content up via children
function InputWrapper({ children }) {
  const [query, setQuery] = useState('');
  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      {children} {/* children don't re-render */}
    </div>
  );
}

function Page() {
  return (
    <InputWrapper>
      <ExpensiveList />
    </InputWrapper>
  );
}

Note Before reaching for React.memo, try restructuring your component tree. Moving state down and lifting content up are free optimizations that often eliminate the problem entirely.

Key Prop for Efficient Updates

syntax
// Stable keys help React identify which items changed
<Item key={item.id} />

// Reset component state by changing the key
<Form key={selectedUserId} />
example
// Using key to reset component state
function UserEditor({ userId }) {
  // When userId changes, React destroys the old Form
  // and mounts a fresh one with clean state
  return <ProfileForm key={userId} userId={userId} />;
}

function ProfileForm({ userId }) {
  const [name, setName] = useState('');
  const [bio, setBio] = useState('');

  useEffect(() => {
    fetchUser(userId).then(user => {
      setName(user.name);
      setBio(user.bio);
    });
  }, [userId]);

  return (
    <form>
      <input value={name} onChange={e => setName(e.target.value)} />
      <textarea value={bio} onChange={e => setBio(e.target.value)} />
    </form>
  );
}

Note Changing a component's key forces React to unmount and remount it, resetting all internal state. This is a clean way to 'reset' a form or component when switching between items without manually clearing each state variable.

React Profiler

syntax
<Profiler id="name" onRender={callback}>
  <Component />
</Profiler>
// callback(id, phase, actualDuration, baseDuration, startTime, commitTime)
example
import { Profiler } from 'react';

function onRenderCallback(
  id,
  phase,        // 'mount' or 'update'
  actualDuration, // time spent rendering
  baseDuration,   // estimated time without memoization
  startTime,
  commitTime
) {
  console.log(`${id} [${phase}]: ${actualDuration.toFixed(1)}ms`);
}

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

Note Use the Profiler component or React DevTools Profiler tab to identify slow renders. Focus on components with high actualDuration. The baseDuration shows the worst-case cost without any memoization.

List Virtualization

syntax
// Render only visible items in long lists
// Use react-window or react-virtuoso
example
// Concept: only render items in the viewport
import { FixedSizeList } from 'react-window';

function VirtualizedUserList({ users }) {
  const Row = ({ index, style }) => (
    <div style={style} className="user-row">
      <span>{users[index].name}</span>
      <span>{users[index].email}</span>
    </div>
  );

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

// Renders ~10-15 visible rows instead of 10,000

Note For lists with hundreds or thousands of items, virtualization dramatically improves performance by only rendering visible rows. react-window is lightweight; react-virtuoso supports dynamic heights. Measure first -- small lists do not need this.