Skip to main content

Overview

Once you’ve identified performance bottlenecks using profiling tools, the next step is applying targeted optimization techniques. This guide covers the most impactful optimization strategies for modern web applications.
Optimization should be data-driven. Always measure before and after to verify improvements and avoid premature optimization.

Component memoization strategies

Memoization prevents unnecessary re-renders by caching component outputs and computation results.

React.memo

Wraps functional components to prevent re-renders when props haven’t changed.
import { memo } from 'react';

const ExpensiveComponent = memo(({ data, onSelect }) => {
  console.log('Rendering ExpensiveComponent');
  
  return (
    <div>
      {data.map(item => (
        <div key={item.id} onClick={() => onSelect(item)}>
          {item.name}
        </div>
      ))}
    </div>
  );
});
Component only re-renders when data or onSelect changes.

useMemo and useCallback

import { useMemo } from 'react';

function DataTable({ data, filters }) {
  // Expensive computation memoized based on dependencies
  const filteredData = useMemo(() => {
    console.log('Filtering data...');
    return data.filter(item => {
      return Object.entries(filters).every(([key, value]) => {
        return item[key] === value;
      });
    });
  }, [data, filters]);
  
  const sortedData = useMemo(() => {
    console.log('Sorting data...');
    return [...filteredData].sort((a, b) => a.name.localeCompare(b.name));
  }, [filteredData]);
  
  return (
    <table>
      {sortedData.map(item => (
        <tr key={item.id}>
          <td>{item.name}</td>
        </tr>
      ))}
    </table>
  );
}
Don’t memoize everything. Memoization has overhead. Only memoize expensive computations or when preventing re-renders of child components.

Virtualized lists and windowing

Virtualization renders only visible items in large lists, dramatically improving performance.

Why virtualization matters

Without virtualization

Rendering 10,000 items creates 10,000 DOM nodes, causing:
  • Slow initial render
  • High memory usage
  • Sluggish scrolling
  • Browser freezing

With virtualization

Only renders ~20 visible items at a time:
  • Fast initial render
  • Low memory footprint
  • Smooth 60fps scrolling
  • Responsive UI

Using react-window

import { FixedSizeList } from 'react-window';

function VirtualList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      {items[index].name}
    </div>
  );
  
  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={50}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}
Best for lists with uniform item heights.
Use react-window for most cases. For more advanced features like dynamic loading and sticky headers, consider react-virtualized.

Image optimization

Images are often the largest assets on web pages. Proper optimization is crucial for performance.

Image formats

Superior compression with excellent quality. Supported in all modern browsers.
<picture>
  <source srcset="image.webp" type="image/webp" />
  <source srcset="image.jpg" type="image/jpeg" />
  <img src="image.jpg" alt="Description" />
</picture>
Benefits: 25-35% smaller than JPEG, supports transparency and animation.
Even better compression than WebP, but limited browser support.
<picture>
  <source srcset="image.avif" type="image/avif" />
  <source srcset="image.webp" type="image/webp" />
  <img src="image.jpg" alt="Description" />
</picture>
Benefits: Up to 50% smaller than JPEG with better quality.
Perfect for icons, logos, and illustrations that need to scale.
// Inline SVG for maximum control
<svg width="24" height="24" viewBox="0 0 24 24">
  <path d="M12 2L2 7v10c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V7l-10-5z" />
</svg>

// External SVG
<img src="icon.svg" alt="Icon" />
Benefits: Infinitely scalable, small file size, CSS/JS controllable.

Lazy loading

<!-- Browser handles lazy loading automatically -->
<img src="large-image.jpg" alt="Description" loading="lazy" />

<!-- Eager loading for above-the-fold images -->
<img src="hero.jpg" alt="Hero" loading="eager" />

Responsive images

<!-- Different images for different screen sizes -->
<picture>
  <source
    media="(min-width: 1200px)"
    srcset="image-large.jpg 1x, [email protected] 2x"
  />
  <source
    media="(min-width: 768px)"
    srcset="image-medium.jpg 1x, [email protected] 2x"
  />
  <img
    src="image-small.jpg"
    srcset="image-small.jpg 1x, [email protected] 2x"
    alt="Responsive image"
  />
</picture>

<!-- Size-based selection -->
<img
  src="image-800.jpg"
  srcset="
    image-400.jpg 400w,
    image-800.jpg 800w,
    image-1200.jpg 1200w
  "
  sizes="(max-width: 600px) 400px, (max-width: 1000px) 800px, 1200px"
  alt="Auto-sized image"
/>

Font optimization

Web fonts can significantly impact page load performance if not optimized properly.

Subsetting

Reduce font file size by including only needed characters.
# Using pyftsubset from fonttools
pip install fonttools brotli

# Create subset with only Latin characters
pyftsubset font.ttf \
  --output-file=font-subset.woff2 \
  --flavor=woff2 \
  --layout-features="*" \
  --unicodes=U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD

Preloading fonts

<!DOCTYPE html>
<html>
  <head>
    <!-- Preload critical fonts -->
    <link
      rel="preload"
      href="/fonts/main-font.woff2"
      as="font"
      type="font/woff2"
      crossorigin
    />
    
    <style>
      @font-face {
        font-family: 'MainFont';
        src: url('/fonts/main-font.woff2') format('woff2');
        font-weight: 400;
        font-display: swap;
      }
    </style>
  </head>
</html>

Font display strategies

font-display: swap

Show fallback font immediately, swap when custom font loads.Best for: Most use cases. Avoids invisible text.

font-display: optional

Use custom font only if already cached, otherwise use fallback.Best for: Performance-critical pages where font is not essential.

font-display: fallback

Brief block period, then show fallback, swap if font loads quickly.Best for: Balance between swap and optional.

font-display: block

Wait for font to load before showing text (up to 3s).Best for: When font is critical to design (use sparingly).

Bundle analysis and tree shaking

Analyze and optimize your JavaScript bundles to reduce load times.

Analyzing bundles

1

Install analyzer

npm install --save-dev webpack-bundle-analyzer
2

Configure webpack

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      openAnalyzer: false,
      reportFilename: 'bundle-report.html'
    })
  ]
};
3

Run build

npm run build
Open bundle-report.html to visualize your bundle composition.
4

Identify issues

Look for:
  • Large dependencies that can be replaced
  • Duplicate modules
  • Unused code
  • Opportunity for code splitting

Tree shaking

Eliminate unused code from bundles.
import _ from 'lodash';

const result = _.debounce(fn, 300);
Tree shaking only works with ES modules (import/export). CommonJS modules (require/module.exports) cannot be tree-shaken.

Code splitting strategies

Split your code into smaller chunks to reduce initial load time.

Route-based splitting

import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

// Lazy load route components
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/settings" element={<Settings />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

Component-based splitting

import { lazy, Suspense, useState } from 'react';

// Heavy components loaded only when needed
const VideoPlayer = lazy(() => import('./VideoPlayer'));
const RichTextEditor = lazy(() => import('./RichTextEditor'));
const DataVisualization = lazy(() => import('./DataVisualization'));

function ContentPage() {
  const [activeTab, setActiveTab] = useState('video');
  
  return (
    <div>
      <Tabs value={activeTab} onChange={setActiveTab}>
        <Tab value="video">Video</Tab>
        <Tab value="editor">Editor</Tab>
        <Tab value="charts">Charts</Tab>
      </Tabs>
      
      <Suspense fallback={<Spinner />}>
        {activeTab === 'video' && <VideoPlayer />}
        {activeTab === 'editor' && <RichTextEditor />}
        {activeTab === 'charts' && <DataVisualization />}
      </Suspense>
    </div>
  );
}

Vendor splitting

// webpack.config.js
module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        // Separate vendor bundle
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 10
        },
        // Separate common code
        common: {
          minChunks: 2,
          name: 'common',
          chunks: 'all',
          priority: 5,
          reuseExistingChunk: true
        },
        // Separate React libraries
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: 'react',
          chunks: 'all',
          priority: 20
        }
      }
    }
  }
};
Aim for chunk sizes between 20-200KB. Too many small chunks increase HTTP overhead, while large chunks defeat the purpose of splitting.

Performance optimization checklist

1

Measure baseline

  • Run Lighthouse audit
  • Profile with Chrome DevTools
  • Record Core Web Vitals
2

Optimize rendering

  • Memoize expensive components
  • Virtualize long lists
  • Avoid layout thrashing
3

Optimize assets

  • Convert images to WebP/AVIF
  • Implement lazy loading
  • Subset and preload fonts
4

Optimize bundles

  • Analyze bundle composition
  • Enable tree shaking
  • Implement code splitting
5

Verify improvements

  • Re-run Lighthouse
  • Compare metrics
  • Monitor real user metrics

Next steps

Profiling tools

Learn to identify performance bottlenecks using Chrome DevTools and React Profiler

Critical rendering path

Optimize initial page load by eliminating render-blocking resources

Build docs developers (and LLMs) love