DEV Community

Alex Chen
Alex Chen

Posted on

React Performance: 8 Fixes That Actually Matter (2026)

React Performance: 8 Fixes That Actually Matter (2026)

I audited 15 React apps. These 8 optimizations appeared in every slow one.

1. Stop Re-rendering Everything

// ❌ BAD: Every keystroke re-renders the entire form
function Form() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  return (
    <form>
      <ExpensiveChart data={heavyData} /> {/* Re-renders on every keystroke! */}
      <input value={name} onChange={e => setName(e.target.value)} />
      <input value={email} onChange={e => setEmail(e.target.value)} />
    </form>
  );
}

// ✅ GOOD: Memoize expensive components
function Form() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  return (
    <form>
      <MemoizedChart data={heavyData} />
      <input value={name} onChange={e => setName(e.target.value)} />
      <input value={email} onChange={e => setEmail(e.target.value)} />
    </form>
  );
}

const MemoizedChart = React.memo(function Chart({ data }) {
  return <ExpensiveChart data={data} />;
});
Enter fullscreen mode Exit fullscreen mode

2. Use the Right State Management

// ❌ BAD: Context re-renders ALL consumers on ANY change
const ThemeContext = createContext();
const UserContext = createContext();

function App() {
  const [theme, setTheme] = useState('dark');
  const [user, setUser] = useState(null);

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <UserContext.Provider value={{ user, setUser }}>
        <Dashboard /> {/* Re-renders when theme OR user changes */}
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

// ✅ GOOD: Split contexts + use libraries for complex state
// Option 1: Separate contexts (simple apps)
<ThemeProvider>
  <UserProvider>
    <Dashboard />
  </UserProvider>
</ThemeProvider>

// Option 2: Use Zustand (complex apps)
import { create } from 'zustand';

const useTheme = create((set) => ({
  theme: 'dark',
  setTheme: (t) => set({ theme: t }),
}));

// Components only re-render when THEIR slice changes
function ThemeToggle() {
  const theme = useTheme(s => s.theme); // Only re-renders if theme changes
  const setTheme = useTheme(s => s.setTheme);
  return <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>Toggle</button>;
}
Enter fullscreen mode Exit fullscreen mode

3. Lazy Load Routes

// ❌ BAD: All page components loaded upfront
import Home from './pages/Home';
import Dashboard from './pages/Dashboard';
import Settings from './pages/Settings';
// Bundle includes ALL pages even if user never visits them

// ✅ GOOD: Lazy load each route
import { lazy, Suspense } from 'react';

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

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}
Enter fullscreen mode Exit fullscreen mode

Impact: Dashboard app bundle went from 1.2MB to 180KB initial load.

4. Virtualize Long Lists

// ❌ BAD: Renders ALL 10,000 items
function UserList({ users }) {
  return (
    <div>
      {users.map(user => (
        <UserRow key={user.id} user={user} />
      ))}
    </div>
  );
}

// ✅ GOOD: Only renders visible items
import { useVirtualizer } from '@tanstack/react-virtual';

function UserList({ users }) {
  const parentRef = useRef(null);

  const virtualizer = useVirtualizer({
    count: users.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 48, // Estimated row height
  });

  return (
    <div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize() }}>
        {virtualizer.getVirtualItems().map(virtualItem => (
          <UserRow 
            key={virtualItem.key}
            user={users[virtualItem.index]}
            style={{ 
              position: 'absolute', 
              top: 0, 
              transform: `translateY(${virtualItem.start}px)` 
            }}
          />
        ))}
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Impact: 10,000 row table: render time dropped from 4.2s to 16ms.

5. Debounce Expensive Operations

// ❌ BAD: Search fires on every keystroke
function SearchBar() {
  const [query, setQuery] = useState('');

  const handleChange = (e) => {
    setQuery(e.target.value);
    searchAPI(e.target.value); // API call on EVERY keystroke!
  };

  return <input value={query} onChange={handleChange} />;
}

// ✅ GOOD: Debounce the API call
import { useDebouncedValue } from '@mantine/hooks';

function SearchBar() {
  const [query, setQuery] = useState('');
  const [debouncedQuery] = useDebouncedValue(query, 300);

  useEffect(() => {
    if (debouncedQuery) {
      searchAPI(debouncedQuery);
    }
  }, [debouncedQuery]);

  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
Enter fullscreen mode Exit fullscreen mode

6. Optimize Images

// ❌ BAD: Full resolution images
<img src="/hero-banner.jpg" alt="Banner" />

// ✅ GOOD: Responsive images + lazy loading + WebP
<picture>
  <source srcSet="/hero-banner.avif" type="image/avif" />
  <source srcSet="/hero-banner.webp" type="image/webp" />
  <img 
    src="/hero-banner.jpg" 
    alt="Banner"
    loading="lazy"
    decoding="async"
    width={1200}
    height={600}
  />
</picture>
Enter fullscreen mode Exit fullscreen mode

Impact: Page weight dropped from 8MB to 1.2MB after converting to WebP + lazy loading.

7. Use useCallback and useMemo Wisely

// ✅ DO: Memoize callbacks passed to children
function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []); // Stable reference — child won't re-render

  return <MemoizedChild onClick={handleClick} count={count} />;
}

// ❌ DON'T: Memoize everything "just in case"
function BadComponent() {
  const value = useMemo(() => 2 + 2, []); // Overkill — this is trivial
  const fn = useCallback(() => 'hello', []); // Overkill — no child depends on it
  return <div>{value}</div>;
}

// Rule: Only memoize if the value is expensive to compute
// OR if it's passed to a memoized child component
Enter fullscreen mode Exit fullscreen mode

8. Measure Before Optimizing

// React DevTools Profiler — built-in!
// 1. Open React DevTools → Profiler tab
// 2. Click record
// 3. Interact with your app
// 4. Stop recording
// 5. See exactly which components re-rendered and why

// Chrome Performance tab for non-React performance:
// 1. F12 → Performance tab
// 2. Record
// 3. Interact
// 4. Stop
// 5. Check for long tasks (>50ms), layout thrashing, paint

// Web Vitals in code:
import { onCLS, onFID, onLCP, onINP } from 'web-vitals';

onCLS(console.log);  // Cumulative Layout Shift (< 0.1)
onFID(console.log);  // First Input Delay (< 100ms)
onLCP(console.log);  // Largest Contentful Paint (< 2.5s)
onINP(console.log);  // Interaction to Next Paint (< 200ms)
Enter fullscreen mode Exit fullscreen mode

The Quick Wins Checklist

Fix Effort Impact Do It First?
Lazy load routes Low High ✅
Image optimization Low High ✅
Debounce inputs Low Medium ✅
Virtualize lists Medium High For long lists
Split contexts Medium High For complex state
React.memo Low Medium Only where needed
useCallback/useMemo Low Low Only when needed
State library Medium High For complex apps

The golden rule: Measure first, optimize second. Don't guess.


What's the biggest React performance fix you've made? Share your story!

Follow @armorbreak for more React tips.

Top comments (0)