DEV Community

Cover image for React Performance Optimization: How I Reduced Load Time by 20%
Haris Siddiqui
Haris Siddiqui

Posted on

React Performance Optimization: How I Reduced Load Time by 20%

The Problem

During my time as a ReactJS Frontend Developer, I inherited a React application that was frustratingly slow. Users were complaining about long loading times, and our bounce rate was climbing. The stakeholders gave me a clear challenge: "Make it faster, or we'll consider a complete rewrite."

The numbers were brutal:

  • Initial load time: ~4.2 seconds
  • Time to Interactive (TTI): ~6.8 seconds
  • Bundle size: 2.1 MB
  • Lighthouse Performance Score: 34/100

After implementing the strategies I'll share below, we achieved:

  • Load time: 3.1 seconds (26% improvement)
  • TTI: 4.9 seconds (28% improvement)
  • Bundle size: 1.4 MB (33% reduction)
  • Lighthouse Score: 78/100 (129% improvement)

Let me walk you through exactly how we did it.

Step 1: Measure First, Optimize Second

Before touching any code, I established baseline metrics using multiple tools:

# Install performance monitoring tools
npm install --save-dev webpack-bundle-analyzer
npm install --save-dev lighthouse
Enter fullscreen mode Exit fullscreen mode

Measurement Setup:

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

module.exports = {
  // ... other config
  plugins: [
    process.env.ANALYZE && new BundleAnalyzerPlugin()
  ].filter(Boolean)
};

// package.json
{
  "scripts": {
    "analyze": "ANALYZE=true npm run build"
  }
}
Enter fullscreen mode Exit fullscreen mode

Key insight: You can't improve what you don't measure. The bundle analyzer immediately showed that our largest chunk was a charting library we barely used.

Step 2: Code Splitting and Lazy Loading

The biggest win came from splitting our monolithic bundle:

Before:

// App.js - Everything imported upfront
import Dashboard from './components/Dashboard';
import Analytics from './components/Analytics';
import Reports from './components/Reports';
import Settings from './components/Settings';

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/analytics" element={<Analytics />} />
        <Route path="/reports" element={<Reports />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Router>
  );
}
Enter fullscreen mode Exit fullscreen mode

After:

// App.js - Lazy load route components
import { lazy, Suspense } from 'react';

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

function App() {
  return (
    <Router>
      <Suspense fallback={<div className="loading-spinner">Loading...</div>}>
        <Routes>
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/analytics" element={<Analytics />} />
          <Route path="/reports" element={<Reports />} />
          <Route path="/settings" element={<Settings />} />
        </Routes>
      </Suspense>
    </Router>
  );
}
Enter fullscreen mode Exit fullscreen mode

Result: Initial bundle size reduced from 2.1MB to 1.2MB immediately.

Step 3: Component-Level Optimizations

Memoization Strategy

I identified components that were re-rendering unnecessarily:

// Before - Re-renders on every parent update
const UserCard = ({ user, onEdit }) => {
  return (
    <div className="user-card">
      <img src={user.avatar} alt={user.name} />
      <h3>{user.name}</h3>
      <button onClick={() => onEdit(user.id)}>Edit</button>
    </div>
  );
};

// After - Only re-renders when user data changes
const UserCard = React.memo(({ user, onEdit }) => {
  return (
    <div className="user-card">
      <img src={user.avatar} alt={user.name} />
      <h3>{user.name}</h3>
      <button onClick={() => onEdit(user.id)}>Edit</button>
    </div>
  );
}, (prevProps, nextProps) => {
  return prevProps.user.id === nextProps.user.id && 
         prevProps.user.name === nextProps.user.name;
});
Enter fullscreen mode Exit fullscreen mode

useMemo for Expensive Calculations

// Before - Recalculated on every render
const Dashboard = ({ data }) => {
  const processedData = data.map(item => ({
    ...item,
    calculations: heavyCalculation(item)
  }));

  return <Chart data={processedData} />;
};

// After - Only recalculated when data changes
const Dashboard = ({ data }) => {
  const processedData = useMemo(() => 
    data.map(item => ({
      ...item,
      calculations: heavyCalculation(item)
    })), [data]
  );

  return <Chart data={processedData} />;
};
Enter fullscreen mode Exit fullscreen mode

Step 4: Image Optimization

Images were a major bottleneck. Here's what worked:

// Custom hook for progressive image loading
const useProgressiveImage = (src) => {
  const [loading, setLoading] = useState(true);
  const [imgSrc, setImgSrc] = useState(null);

  useEffect(() => {
    const img = new Image();
    img.onload = () => {
      setImgSrc(src);
      setLoading(false);
    };
    img.src = src;
  }, [src]);

  return { loading, imgSrc };
};

// Component usage
const ImageComponent = ({ src, placeholder, alt }) => {
  const { loading, imgSrc } = useProgressiveImage(src);

  return (
    <div className="image-container">
      {loading ? (
        <img src={placeholder} alt={alt} className="placeholder" />
      ) : (
        <img src={imgSrc} alt={alt} className="loaded" />
      )}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Step 5: API Call Optimization

Request Deduplication

// Custom hook to prevent duplicate API calls
const useApiCall = (url) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const requestCache = useRef(new Map());

  const fetchData = useCallback(async () => {
    if (requestCache.current.has(url)) {
      return requestCache.current.get(url);
    }

    setLoading(true);
    try {
      const response = await fetch(url);
      const result = await response.json();

      requestCache.current.set(url, result);
      setData(result);
      return result;
    } catch (error) {
      console.error('API call failed:', error);
    } finally {
      setLoading(false);
    }
  }, [url]);

  return { data, loading, fetchData };
};
Enter fullscreen mode Exit fullscreen mode

Data Prefetching

// Prefetch data for likely next pages
const Dashboard = () => {
  const { data: currentData } = useApiCall('/api/dashboard');

  useEffect(() => {
    // Prefetch analytics data (user likely to visit next)
    const timer = setTimeout(() => {
      fetch('/api/analytics').then(response => response.json());
    }, 2000);

    return () => clearTimeout(timer);
  }, []);

  return <DashboardContent data={currentData} />;
};
Enter fullscreen mode Exit fullscreen mode

Step 6: Bundle Optimization

Tree Shaking Improvements

// Before - Importing entire library
import _ from 'lodash';
import moment from 'moment';

// After - Import only what you need
import { debounce, throttle } from 'lodash';
import dayjs from 'dayjs'; // Smaller alternative to moment
Enter fullscreen mode Exit fullscreen mode

Dynamic Imports for Heavy Libraries

// Load heavy chart library only when needed
const ChartComponent = ({ data }) => {
  const [ChartLibrary, setChartLibrary] = useState(null);

  useEffect(() => {
    import('react-chartjs-2').then((module) => {
      setChartLibrary(() => module.Line);
    });
  }, []);

  if (!ChartLibrary) {
    return <div>Loading chart...</div>;
  }

  return <ChartLibrary data={data} />;
};
Enter fullscreen mode Exit fullscreen mode

Step 7: Service Worker Implementation

// public/sw.js - Basic caching strategy
const CACHE_NAME = 'app-v1';
const urlsToCache = [
  '/',
  '/static/css/main.css',
  '/static/js/main.js'
];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then((cache) => cache.addAll(urlsToCache))
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then((response) => response || fetch(event.request))
  );
});
Enter fullscreen mode Exit fullscreen mode

Measuring the Impact

Performance Monitoring Setup:

// utils/performance.js
export const measureComponentPerformance = (componentName) => {
  return function(WrappedComponent) {
    return function MeasuredComponent(props) {
      useEffect(() => {
        const startTime = performance.now();

        return () => {
          const endTime = performance.now();
          console.log(`${componentName} render time: ${endTime - startTime}ms`);
        };
      });

      return <WrappedComponent {...props} />;
    };
  };
};

// Usage
export default measureComponentPerformance('Dashboard')(Dashboard);
Enter fullscreen mode Exit fullscreen mode

The Results

After implementing these optimizations over 3 weeks:

Metric Before After Improvement
Load Time 4.2s 3.1s 26% faster
Time to Interactive 6.8s 4.9s 28% faster
Bundle Size 2.1MB 1.4MB 33% smaller
Lighthouse Score 34/100 78/100 129% better

Key Lessons Learned

  1. Measure everything - You can't optimize what you can't see
  2. Start with the biggest wins - Code splitting gave us immediate 40% bundle reduction
  3. Don't over-optimize - Some micro-optimizations aren't worth the complexity
  4. User experience matters most - Sometimes a loading spinner is better than a frozen UI

Tools That Made the Difference

  • React DevTools Profiler - Identifying slow components
  • Webpack Bundle Analyzer - Visualizing bundle composition
  • Chrome DevTools - Network and performance analysis
  • Lighthouse CI - Automated performance testing

What's Next?

  • Implementing React 18's concurrent features
  • Exploring React Server Components
  • Adding more sophisticated caching strategies
  • A/B testing different loading patterns

Performance optimization is a journey, not a destination. The techniques above got us significant improvements, but there's always more to discover.

What performance challenges are you facing in your React apps? Share your experiences in the comments!

Tags

#react #performance #optimization #webdev #javascript #frontend #lighthouse

Top comments (0)