DEV Community

Alex Chen
Alex Chen

Posted on

React Performance: 8 Fixes That Actually Matter (2026)

React Performance: 8 Fixes That Actually Matter (2026)

Forget premature memoization. These are the optimizations that move the needle on real apps.

Before You Optimize Anything

Step 1: Measure
Step 2: Identify the bottleneck  
Step 3: Fix ONLY the bottleneck
Step 4: Measure again
Step 5: Stop optimizing
Enter fullscreen mode Exit fullscreen mode

If you can't measure it, don't optimize it. React DevTools Profiler is your friend.

Fix 1: Stop Re-Creating Objects in Render

// ❌ BAD: New object every render
function UserList() {
  return users.map(user => (
    <UserCard 
      key={user.id}
      style={{ padding: 16, margin: 8 }}  // New object every time
      onClick={() => handleDelete(user.id)}  // New function every time
      config={{ theme: 'dark', size: 'lg' }}  // New object every time
    />
  ));
}

// ✅ GOOD: Stable references
const CARD_STYLE = { padding: 16, margin: 8 };
const CARD_CONFIG = { theme: 'dark', size: 'lg' };

function UserList() {
  return users.map(user => (
    <UserCard 
      key={user.id}
      style={CARD_STYLE}
      onClick={() => handleDelete(user.id)}
      config={CARD_CONFIG}
    />
  ));
}
Enter fullscreen mode Exit fullscreen mode

Impact: Reduces unnecessary re-renders of child components.

Fix 2: Move Expensive Calculations to useMemo (Sparingly)

// ❌ BAD: Re-calculates on every render
function Dashboard({ orders }) {
  const stats = {
    total: orders.reduce((sum, o) => sum + o.total, 0),
    average: orders.reduce((sum, o) => sum + o.total, 0) / orders.length,
    topProduct: getMostOrderedProduct(orders), // Expensive!
    byCategory: groupByCategory(orders),       // Expensive!
  };

  return <StatsView stats={stats} />;
}

// ✅ GOOD: Memoize expensive calculations
function Dashboard({ orders }) {
  const stats = useMemo(() => ({
    total: orders.reduce((sum, o) => sum + o.total, 0),
    average: orders.reduce((sum, o) => sum + o.total, 0) / (orders.length || 1),
    topProduct: getMostOrderedProduct(orders),
    byCategory: groupByCategory(orders),
  }), [orders]);

  return <StatsView stats={stats} />;
}
Enter fullscreen mode Exit fullscreen mode

When to use: Only when the calculation is actually slow (>1ms). useMemo itself has overhead.

Fix 3: Virtualize Long Lists

// ❌ BAD: Renders ALL items (1000+ DOM nodes)
function UserList({ users }) {
  return (
    <div className="user-list">
      {users.map(user => <UserCard 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: () => 80, // Estimated item height
    overscan: 5, // Render 5 extra items above/below viewport
  });

  return (
    <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
        {virtualizer.getVirtualItems().map(virtualRow => (
          <div
            key={virtualRow.key}
            style={{
              position: 'absolute',
              top: virtualRow.start,
              height: virtualRow.size,
            }}
          >
            <UserCard user={users[virtualRow.index]} />
          </div>
        ))}
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Impact: 1000 items → 20 DOM nodes. Massive difference.

Fix 4: Lazy Load Routes

// ❌ BAD: All code loaded upfront
import Dashboard from './pages/Dashboard';
import Settings from './pages/Settings';
import Reports from './pages/Reports';

const routes = [
  { path: '/dashboard', element: <Dashboard /> },
  { path: '/settings', element: <Settings /> },
  { path: '/reports', element: <Reports /> },
];

// ✅ GOOD: Code-split by route
import { lazy, Suspense } from 'react';

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

const routes = [
  { 
    path: '/dashboard', 
    element: (
      <Suspense fallback={<Loading />}>
        <Dashboard />
      </Suspense>
    ) 
  },
  // ...
];
Enter fullscreen mode Exit fullscreen mode

Impact: Initial bundle size drops 40-70% on typical apps.

Fix 5: Batch State Updates

// ❌ BAD: Multiple renders
function handleFormSubmit(data) {
  setFirstName(data.firstName);  // Render 1
  setLastName(data.lastName);    // Render 2
  setEmail(data.email);          // Render 3
  setPhone(data.phone);          // Render 4
  setSaving(true);               // Render 5
}

// ✅ GOOD: Single render
function handleFormSubmit(data) {
  setSaving(true);
  // React 18 batches these automatically
  setFirstName(data.firstName);
  setLastName(data.lastName);
  setEmail(data.email);
  setPhone(data.phone);
}

// ✅ BETTER: Single state update
function handleFormSubmit(data) {
  setSaving(true);
  setFormData(prev => ({
    ...prev,
    ...data,
  }));
}
Enter fullscreen mode Exit fullscreen mode

Note: React 18 auto-batches most state updates. But in async callbacks, use flushSync or combine into one setState if needed.

Fix 6: Use React.memo Correctly

// ❌ WRONG: Wrapping everything
const UserCard = React.memo(function UserCard({ user }) {
  return <div>{user.name}</div>;
});
// This is a micro-optimization if UserCard is fast to render

// ✅ RIGHT: Only wrap expensive components
const ExpensiveChart = React.memo(function ExpensiveChart({ data }) {
  // This component does heavy canvas rendering
  return <CanvasChart data={data} />;
}, (prevProps, nextProps) => {
  // Custom comparison — only re-render if data actually changed
  return prevProps.data.hash === nextProps.data.hash;
});
Enter fullscreen mode Exit fullscreen mode

Rule of thumb: If the component renders in <1ms, React.memo adds more overhead than it saves.

Fix 7: Debounce Search Inputs

import { useState, useEffect, useMemo } from 'react';
import { useDebounce } from './hooks'; // or use-debounce library

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

  // This only fires when user stops typing for 300ms
  const { data: users, isLoading } = useQuery({
    queryKey: ['users', debouncedQuery],
    queryFn: () => searchUsers(debouncedQuery),
    enabled: debouncedQuery.length > 2,
  });

  return (
    <input
      value={query}
      onChange={e => setQuery(e.target.value)}
      placeholder="Search users..."
    />
  );
}

// Simple debounce hook
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}
Enter fullscreen mode Exit fullscreen mode

Fix 8: Image Optimization

// ❌ BAD: Raw image loading
<img src="/hero.jpg" alt="Hero" />

// ✅ GOOD: Lazy loaded + placeholder
import Image from 'next/image'; // Next.js
// OR
import { LazyLoadImage } from 'react-lazy-load-image-component';

<LazyLoadImage
  src="/hero.jpg"
  alt="Hero"
  placeholderSrc="/hero-tiny.jpg"  // 20x20 blurred version
  effect="blur"
  width={800}
  height={400}
/>
Enter fullscreen mode Exit fullscreen mode

Impact: Images are often 60-80% of page weight. Lazy loading + proper sizing = massive improvement.

What NOT to Do

"Optimization" Why It's a Waste
useCallback for every function Overhead > benefit in most cases
React.memo on leaf components Memoization cost > render cost
useMemo for simple calculations Just compute it, it's faster
Moving everything to Web Workers Communication overhead
Premature code splitting Split at route boundaries first

The Performance Checklist

  1. Profile with React DevTools before optimizing anything
  2. Virtualize lists > 100 items
  3. Lazy load routes
  4. Batch state updates
  5. Stable object/function references in render
  6. Debounce search inputs
  7. Optimize images (lazy load, proper sizing, WebP)
  8. React.memo + useMemo only where profiling shows re-renders

The 80/20 rule: 80% of performance issues come from 20% of your code. Find that 20% and fix it. Leave the rest alone.


Follow @armorbreak for more frontend performance tips.

Top comments (0)