perf hacks

React Optimization

Optimize React rendering for better performance and lower INP

React's rendering behavior can significantly impact performance. Understanding when and why components re-render is key to optimization.

REACT RE-RENDER CASCADEParent (state change)Child AChild BChild CWith memo()Child C✓ skippedprops unchangedRe-renderedSkipped (memoized)Parent state change → All children re-render (unless memoized)

Understanding Re-renders

A component re-renders when:

  1. Its state changes
  2. Its parent re-renders
  3. Its context value changes
  4. Its props change (for non-memoized components)
// Parent re-renders → All children re-render
function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>
        Count: {count}
      </button>
      <ExpensiveChild /> {/* Re-renders even though props unchanged */}
    </div>
  );
}

React.memo

Prevent re-renders when props haven't changed:

// ❌ Re-renders on every parent render
function ExpensiveList({ items }) {
  return items.map(item => <ListItem key={item.id} item={item} />);
}

// ✅ Only re-renders when items change
const ExpensiveList = memo(function ExpensiveList({ items }) {
  return items.map(item => <ListItem key={item.id} item={item} />);
});

Custom Comparison

const UserCard = memo(
  function UserCard({ user, onSelect }) {
    return (
      <div onClick={() => onSelect(user.id)}>
        {user.name}
      </div>
    );
  },
  (prevProps, nextProps) => {
    // Only re-render if user ID changes
    return prevProps.user.id === nextProps.user.id;
  }
);

useMemo

Cache expensive computations:

function ProductList({ products, category }) {
  // ❌ Filters on every render
  const filtered = products.filter(p => p.category === category);

  // ✅ Only filters when dependencies change
  const filtered = useMemo(
    () => products.filter(p => p.category === category),
    [products, category]
  );

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

When to Use useMemo

// ✅ Good: Expensive computation
const sortedData = useMemo(
  () => data.sort((a, b) => complexSort(a, b)),
  [data]
);

// ✅ Good: Referential equality for memo'd children
const chartConfig = useMemo(
  () => ({ type: 'bar', data: processData(rawData) }),
  [rawData]
);

// ❌ Unnecessary: Simple computation
const doubled = useMemo(() => count * 2, [count]);

useCallback

Stabilize function references:

function SearchForm({ onSearch }) {
  const [query, setQuery] = useState('');

  // ❌ New function every render
  const handleSubmit = () => onSearch(query);

  // ✅ Stable function reference
  const handleSubmit = useCallback(() => {
    onSearch(query);
  }, [query, onSearch]);

  return (
    <form onSubmit={handleSubmit}>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <MemoizedButton onClick={handleSubmit}>Search</MemoizedButton>
    </form>
  );
}

React Compiler (React 19+)

next.config.js
module.exports = {
  experimental: {
    reactCompiler: true,
  },
};

Now memo, useMemo, useCallback are automatic!

function ProductList({ products, category }) {
  // Compiler automatically memoizes
  const filtered = products.filter(p => p.category === category);
  return <List items={filtered} />;
}

State Management Optimization

Colocate State

// ❌ State too high, causes unnecessary re-renders
function App() {
  const [searchQuery, setSearchQuery] = useState('');

  return (
    <div>
      <Header />  {/* Re-renders on every keystroke */}
      <Search query={searchQuery} setQuery={setSearchQuery} />
      <Content /> {/* Re-renders on every keystroke */}
    </div>
  );
}

// ✅ State colocated with component that uses it
function App() {
  return (
    <div>
      <Header />  {/* Doesn't re-render */}
      <Search />  {/* Owns its own state */}
      <Content /> {/* Doesn't re-render */}
    </div>
  );
}

Split Context

// ❌ Single context causes all consumers to re-render
const AppContext = createContext({ user: null, theme: 'dark', cart: [] });

// ✅ Split contexts by update frequency
const UserContext = createContext(null);
const ThemeContext = createContext('dark');
const CartContext = createContext([]);

// Components only subscribe to what they need
function CartIcon() {
  const cart = useContext(CartContext); // Only re-renders on cart changes
  return <span>{cart.length}</span>;
}

State Selectors with Zustand/Redux

import { useStore } from '@/store';

// ❌ Re-renders on any store change
function UserName() {
  const store = useStore();
  return <span>{store.user.name}</span>;
}

// ✅ Only re-renders when name changes
function UserName() {
  const name = useStore(state => state.user.name);
  return <span>{name}</span>;
}

List Optimization

Virtualization

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

function VirtualList({ items }) {
  const parentRef = useRef(null);

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
    overscan: 5,
  });

  return (
    <div ref={parentRef} className="h-[400px] overflow-auto">
      <div style={{ height: virtualizer.getTotalSize() }}>
        {virtualizer.getVirtualItems().map(virtualRow => (
          <div
            key={virtualRow.key}
            style={{
              position: 'absolute',
              top: virtualRow.start,
              height: virtualRow.size,
            }}
          >
            <ListItem item={items[virtualRow.index]} />
          </div>
        ))}
      </div>
    </div>
  );
}

Stable Keys

// ❌ Bad: Index as key causes full re-mount on reorder
{items.map((item, index) => (
  <Item key={index} data={item} />
))}

// ✅ Good: Stable unique ID
{items.map(item => (
  <Item key={item.id} data={item} />
))}

React Transitions

import { useTransition, useState } from 'react';

function FilterableList({ items }) {
  const [filter, setFilter] = useState('');
  const [filteredItems, setFilteredItems] = useState(items);
  const [isPending, startTransition] = useTransition();

  function handleFilter(value: string) {
    setFilter(value); // Urgent: update input immediately

    startTransition(() => {
      // Non-urgent: can be interrupted
      setFilteredItems(items.filter(item =>
        item.name.toLowerCase().includes(value.toLowerCase())
      ));
    });
  }

  return (
    <div>
      <input
        value={filter}
        onChange={e => handleFilter(e.target.value)}
      />
      <div style={{ opacity: isPending ? 0.7 : 1 }}>
        {filteredItems.map(item => <Item key={item.id} item={item} />)}
      </div>
    </div>
  );
}

useDeferredValue

import { useDeferredValue, useMemo } from 'react';

function SearchResults({ query }) {
  // Deferred value lags behind during heavy renders
  const deferredQuery = useDeferredValue(query);

  const results = useMemo(
    () => expensiveSearch(deferredQuery),
    [deferredQuery]
  );

  const isStale = query !== deferredQuery;

  return (
    <div style={{ opacity: isStale ? 0.7 : 1 }}>
      {results.map(result => <Result key={result.id} {...result} />)}
    </div>
  );
}

Profiling React Performance

React DevTools Profiler

Open React DevTools in Chrome/Firefox
Go to the Profiler tab
Click record and interact with your app
Analyze component render times in the flame graph

Programmatic Profiling

import { Profiler } from 'react';

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

<Profiler id="MyComponent" onRender={onRenderCallback}>
  <MyComponent />
</Profiler>

Checklist

Memoization

Use memo() for expensive pure components Prevent re-renders when props haven't changed.

Use useMemo for expensive computations Cache results of filtering, sorting, or transformations.

Use useCallback for stable function refs Prevent memoized children from re-rendering.

Consider React Compiler (React 19+) Automatic memoization eliminates manual optimization.

State

Colocate state near where it's used Move state down to prevent unnecessary parent re-renders.

Split context by update frequency Separate fast-changing from slow-changing state.

Use selectors with state libraries Subscribe only to specific slices of state.

Avoid unnecessary state Derive values from existing state when possible.

Lists

Virtualize long lists Use @tanstack/react-virtual for large datasets.

Use stable keys Use unique IDs, never array indices for dynamic lists.

Memoize list items Wrap list item components with memo().

Avoid inline object props Create objects outside render or memoize them.

Concurrent Features

Use useTransition for non-urgent updates Keep UI responsive during expensive state updates.

Use useDeferredValue for expensive renders Defer heavy child renders while showing stale data.

Implement Suspense boundaries Stream content progressively for better UX.

Consider streaming SSR Send initial HTML while data loads.

On this page