DEV Community

Alex Chen
Alex Chen

Posted on

React Performance: 8 Fixes That Actually Matter (2026)

React Performance: 8 Fixes That Actually Matter (2026)

Stop over-optimizing. These 8 fixes solve 95% of React performance issues.

How to Measure First

// React DevTools Profiler — record, interact, analyze
// Look for: Long render times, unnecessary re-renders

// Chrome DevTools → Performance tab
// Record → interact → stop → check for long tasks

// Quick code check — count renders
const renderCount = useRef(0);
useEffect(() => {
  renderCount.current++;
  console.log(`Rendered ${renderCount.current} times`);
});
Enter fullscreen mode Exit fullscreen mode

Fix 1: Memoize Expensive Computations

// ❌ Recalculates on EVERY render
function ProductList({ products, filter, sort }) {
  const filtered = products
    .filter(p => p.category === filter)
    .sort((a, b) => a[sort] - b[sort]);

  return filtered.map(p => <ProductCard key={p.id} {...p} />);
}

// ✅ Only recalculates when dependencies change
function ProductList({ products, filter, sort }) {
  const filtered = useMemo(() => 
    products
      .filter(p => p.category === filter)
      .sort((a, b) => a[sort] - b[sort]),
    [products, filter, sort]
  );

  return filtered.map(p => <ProductCard key={p.id} {...p} />);
}
Enter fullscreen mode Exit fullscreen mode

When to use: Array operations on 100+ items, complex calculations, expensive string parsing.

Fix 2: Prevent Unnecessary Callback Recreation

// ❌ New function reference every render → child re-renders
function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      <ExpensiveChild onClick={() => console.log('clicked')} />
      {/* ↑ New function every render! */}
    </div>
  );
}

// ✅ Stable function reference
function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = useCallback(() => console.log('clicked'), []);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      <ExpensiveChild onClick={handleClick} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Fix 3: Lazy Load Routes and Heavy Components

import { lazy, Suspense } from 'react';

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

// ✅ Load on demand
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Reports = lazy(() => import('./pages/Reports'));

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
        <Route path="/reports" element={<Reports />} />
      </Routes>
    </Suspense>
  );
}

// Also works for heavy components within a page
const HeavyChart = lazy(() => import('./components/HeavyChart'));
Enter fullscreen mode Exit fullscreen mode

Fix 4: Virtualize Long Lists

// ❌ Renders 10,000 items → slow scroll, high memory
function UserList({ users }) {
  return (
    <div>
      {users.map(user => <UserRow key={user.id} {...user} />)}
    </div>
  );
}

// ✅ Only renders visible items (~20 at a time)
import { FixedSizeList as List } from 'react-window';

function UserList({ users }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      <UserRow {...users[index]} />
    </div>
  );

  return (
    <List
      height={600}           // Container height
      itemCount={users.length}
      itemSize={48}           // Row height
      width="100%"
    >
      {Row}
    </List>
  );
}

// Also: react-virtuoso (more features, easier API)
Enter fullscreen mode Exit fullscreen mode

Fix 5: Optimize Context Usage

// ❌ Single context → entire tree re-renders on any change
const AppContext = createContext();

function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [notifications, setNotifications] = useState([]);

  // ANY of these changing → ALL consumers re-render
  return (
    <AppContext.Provider value={{ user, setUser, theme, setTheme, notifications, setNotifications }}>
      {children}
    </AppContext.Provider>
  );
}

// ✅ Split into separate contexts
const UserContext = createContext();
const ThemeContext = createContext();
const NotificationContext = createContext();

function AppProvider({ children }) {
  return (
    <UserProvider>
      <ThemeProvider>
        <NotificationProvider>
          {children}
        </NotificationProvider>
      </ThemeProvider>
    </UserProvider>
  );
}

// Now: theme change only re-renders theme consumers
Enter fullscreen mode Exit fullscreen mode

Fix 6: Use React.memo for Expensive Components

// ❌ Re-renders even when props haven't changed
function ExpensiveChart({ data, width, height }) {
  // Heavy rendering...
}

// ✅ Only re-renders when props change
const ExpensiveChart = React.memo(function ExpensiveChart({ data, width, height }) {
  // Heavy rendering...
});

// With custom comparison (if default shallow compare isn't enough)
const ExpensiveChart = React.memo(
  function ExpensiveChart({ data, width, height }) {
    // Heavy rendering...
  },
  (prevProps, nextProps) => {
    return prevProps.data.id === nextProps.data.id; // Only re-render if data ID changes
  }
);
Enter fullscreen mode Exit fullscreen mode

Fix 7: Debounce Rapid Updates

import { useDebouncedCallback } from 'use-debounce';

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

  useEffect(() => {
    fetchResults(query).then(setResults); // Fires 60+ times per second while typing
  }, [query]);
}

// ✅ Debounce — wait 300ms after last keystroke
function Search() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  const debouncedFetch = useDebouncedCallback(
    (q) => fetchResults(q).then(setResults),
    300
  );

  const handleChange = (e) => {
    setQuery(e.target.value);
    debouncedFetch(e.target.value);
  };
}

// Also: debounce scroll handlers, resize handlers, drag handlers
Enter fullscreen mode Exit fullscreen mode

Fix 8: Image Optimization

// ❌ Unoptimized images
<img src="/hero.jpg" alt="Hero" />

// ✅ Modern approach
<img
  src="/hero-800.jpg"
  srcSet="/hero-400.jpg 400w, /hero-800.jpg 800w, /hero-1200.jpg 1200w"
  sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
  alt="Hero"
  loading="lazy"
  decoding="async"
  width={800}
  height={400}
/>

// ✅ Even better: next/image (if using Next.js)
// ✅ Or: use a CDN with automatic optimization (Cloudinary, imgix)
Enter fullscreen mode Exit fullscreen mode

What NOT to Optimize

Don't prematurely memoize everything. Profile first!

✅ DO optimize:
- Lists with 100+ items
- Components with heavy calculations
- Components that re-render unnecessarily (check with Profiler)
- Route-level code splitting (always)

❌ DON'T optimize (usually):
- Components that render once or twice
- Simple JSX with no calculations
- Event handlers that aren't passed as props
- Components with primitive props (already stable)
Enter fullscreen mode Exit fullscreen mode

What React performance tips have saved your app?

Follow @armorbreak for more React content.

Top comments (0)