A comprehensive checklist of performance optimizations every React developer should know.

1. Component Optimization

Use React.memo for Pure Components

Prevent unnecessary re-renders for components that don't need to update:

const ExpensiveComponent = React.memo(({ data }) => {
    return <div>{data.content}</div>;
});

// With custom comparison
const MyComponent = React.memo(({ user }) => {
    return <div>{user.name}</div>;
}, (prevProps, nextProps) => {
    return prevProps.user.id === nextProps.user.id;
});

When to use: Components that receive the same props frequently, or expensive render operations.

useMemo for Expensive Calculations

Cache computed values to avoid recalculation on every render:

const ExpensiveList = ({ items, filter }) => {
    const filteredItems = useMemo(() => {
        return items.filter(item => item.category === filter)
            .sort((a, b) => b.price - a.price);
    }, [items, filter]);
    
    return <div>{filteredItems.map(item => <Item key={item.id} {...item} />)}</div>;
};

Remember: Only use for genuinely expensive operations. Don't over-optimize.

useCallback for Stable Function References

Prevent child components from re-rendering when passing callbacks:

const ParentComponent = () => {
    const [count, setCount] = useState(0);
    
    const handleClick = useCallback(() => {
        console.log('Button clicked');
        // Some logic here
    }, []); // Dependencies array
    
    return <ChildComponent onClick={handleClick} />;
};

When to use: Functions passed to memoized child components, or functions used as dependencies in useEffect.

2. List Rendering Optimization

Always Use Proper Keys

// ❌ Bad - Using index
{items.map((item, index) => <Item key={index} {...item} />)}

// ✅ Good - Using unique ID
{items.map(item => <Item key={item.id} {...item} />)}

Why? Indexes can cause incorrect re-renders when items are added, removed, or reordered.

Virtualize Long Lists

For lists with 100+ items, use virtualization:

import { FixedSizeList } from 'react-window';

const VirtualList = ({ items }) => (
    <FixedSizeList
        height={600}
        itemCount={items.length}
        itemSize={50}
        width="100%"
    >
        {({ index, style }) => (
            <div style={style}>{items[index].name}</div>
        )}
    </FixedSizeList>
);

Libraries: react-window, react-virtualized

3. Code Splitting & Lazy Loading

Lazy Load Route Components

import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./Dashboard'));
const Profile = lazy(() => import('./Profile'));

const App = () => (
    <Suspense fallback={<LoadingSpinner />}>
        <Routes>
            <Route path="/dashboard" element={<Dashboard />} />
            <Route path="/profile" element={<Profile />} />
        </Routes>
    </Suspense>
);

Lazy Load Heavy Components

const HeavyChart = lazy(() => import('./HeavyChart'));

const Dashboard = () => {
    const [showChart, setShowChart] = useState(false);
    
    return (
        <div>
            <button onClick={() => setShowChart(true)}>Show Chart</button>
            {showChart && (
                <Suspense fallback={<div>Loading chart...</div>}>
                    <HeavyChart />
                </Suspense>
            )}
        </div>
    );
};

4. State Management Optimization

Lift State Down, Not Always Up

// ❌ Bad - Unnecessary state at top level
const App = () => {
    const [isModalOpen, setIsModalOpen] = useState(false);
    return <div><Navbar /><Content /><Modal open={isModalOpen} /></div>;
};

// ✅ Good - State where it's needed
const ModalContainer = () => {
    const [isModalOpen, setIsModalOpen] = useState(false);
    return <Modal open={isModalOpen} onClose={() => setIsModalOpen(false)} />;
};

Split Context Providers

// ❌ Bad - One massive context
const AppContext = createContext({ user, theme, notifications, settings });

// ✅ Good - Separate contexts
const UserContext = createContext();
const ThemeContext = createContext();
const NotificationContext = createContext();

Why? Changes to one part won't trigger re-renders for components using other parts.

Use Context Selectors

// For complex contexts, use a selector pattern
const useUser = () => {
    const context = useContext(AppContext);
    return context.user;
};

const useTheme = () => {
    const context = useContext(AppContext);
    return context.theme;
};

5. Image Optimization

Lazy Load Images

<img 
    src={image.url} 
    loading="lazy" 
    alt={image.alt}
/>

Use Modern Formats

<picture>
    <source srcSet={image.avif} type="image/avif" />
    <source srcSet={image.webp} type="image/webp" />
    <img src={image.jpg} alt={image.alt} />
</picture>

Proper Sizing

// Always specify width and height to prevent layout shift
<img 
    src={image.url}
    width={800}
    height={600}
    alt={image.alt}
/>

6. Effect Optimization

Clean Up Effects

useEffect(() => {
    const subscription = api.subscribe(data => {
        setData(data);
    });
    
    // Always clean up!
    return () => {
        subscription.unsubscribe();
    };
}, []);

Debounce Expensive Operations

import { debounce } from 'lodash';

const SearchComponent = () => {
    const [query, setQuery] = useState('');
    
    const debouncedSearch = useMemo(
        () => debounce((value) => {
            // Expensive API call
            fetchResults(value);
        }, 300),
        []
    );
    
    const handleChange = (e) => {
        setQuery(e.target.value);
        debouncedSearch(e.target.value);
    };
    
    return <input value={query} onChange={handleChange} />;
};

7. Bundle Size Optimization

Import Only What You Need

// ❌ Bad
import _ from 'lodash';
import { Button } from '@mui/material';

// ✅ Good
import debounce from 'lodash/debounce';
import Button from '@mui/material/Button';

Analyze Your Bundle

# Create-React-App
npm run build -- --stats
npx webpack-bundle-analyzer build/bundle-stats.json

# Vite
npm run build
npx vite-bundle-visualizer

Use Dynamic Imports for Large Libraries

// Only load when needed
const handleExport = async () => {
    const XLSX = await import('xlsx');
    XLSX.writeFile(workbook, 'data.xlsx');
};

8. Render Optimization

Avoid Inline Functions in Props

// ❌ Bad - Creates new function on every render
<Child onClick={() => handleClick(id)} />

// ✅ Good - Stable function reference
const handleChildClick = useCallback(() => handleClick(id), [id]);
<Child onClick={handleChildClick} />

Avoid Creating Objects in Render

// ❌ Bad - New object every render
<Child style={{ margin: 10, padding: 20 }} />

// ✅ Good - Define outside or use useMemo
const childStyle = { margin: 10, padding: 20 };
<Child style={childStyle} />

9. Third-Party Script Optimization

Load Scripts Asynchronously

useEffect(() => {
    const script = document.createElement('script');
    script.src = 'https://example.com/widget.js';
    script.async = true;
    document.body.appendChild(script);
    
    return () => {
        document.body.removeChild(script);
    };
}, []);

Use Web Workers for Heavy Processing

// worker.js
self.onmessage = (e) => {
    const result = heavyCalculation(e.data);
    self.postMessage(result);
};

// Component
const MyComponent = () => {
    useEffect(() => {
        const worker = new Worker('/worker.js');
        worker.postMessage(data);
        
        worker.onmessage = (e) => {
            setResult(e.data);
        };
        
        return () => worker.terminate();
    }, []);
};

10. Monitoring & Measuring

Use React DevTools Profiler

import { Profiler } from 'react';

<Profiler id="MyComponent" onRender={(id, phase, actualDuration) => {
    console.log(`${id} took ${actualDuration}ms`);
}}>
    <MyComponent />
</Profiler>

Lighthouse Performance Audits

Regular checks:

  • Run Lighthouse in Chrome DevTools
  • Check Core Web Vitals (LCP, FID, CLS)
  • Monitor bundle size over time
  • Test on throttled connections

Performance Monitoring Tools

  • Chrome DevTools Performance - Record runtime performance
  • React DevTools Profiler - Component render times
  • Webpack Bundle Analyzer - Bundle size analysis
  • Web Vitals - Real user metrics

Quick Checklist

Before deploying, verify:

  • All images have loading="lazy" attribute
  • List items use stable, unique keys
  • Heavy components are code-split
  • Expensive calculations wrapped in useMemo
  • Callbacks to memoized children use useCallback
  • Large contexts are split into smaller ones
  • Third-party scripts load asynchronously
  • Bundle size analyzed and optimized
  • Lighthouse score > 90
  • No unnecessary re-renders (check with React DevTools)

Common Anti-Patterns to Avoid

  1. Premature optimization - Measure first, optimize second
  2. Over-memoization - Not everything needs useMemo/useCallback
  3. Large context providers - Split into logical chunks
  4. Inline styles - Extract to constants or CSS
  5. Massive component trees - Code split and lazy load
  6. Fetching in useEffect - Use proper data fetching libraries (React Query, SWR)
  7. Ignoring bundle size - Regularly audit with analyzer tools

Advanced Tips

Use Transition API (React 18+)

import { useTransition } from 'react';

const [isPending, startTransition] = useTransition();

const handleTabChange = (tab) => {
    startTransition(() => {
        setTab(tab); // Non-urgent update
    });
};

Optimize Re-renders with useId

// Stable IDs across server/client
const id = useId();
<input id={id} aria-describedby={`${id}-description`} />

Consider Server Components (Next.js 13+)

Move non-interactive parts to server components for zero JS shipped to client.

Remember

Test on real devices, real networks, and real scenarios. Your users will thank you.


Last updated: October 2025