React Performance Optimization: From 3s to 300ms Load Time

January 22, 2025

9 min read

ReactPerformanceJavaScriptWeb Vitals

React Performance Optimization: From 3s to 300ms Load Time

When I launched the initial version of my travel platform, users complained about slow load times. The homepage took 3+ seconds to become interactive. After optimization, I got it down to <300ms.

Here's exactly how I did it.

The Performance Problem

Initial metrics were terrible:

  • First Contentful Paint: 2.8s
  • Time to Interactive: 4.1s
  • Total Bundle Size: 847KB (gzipped)
  • Lighthouse Score: 62/100

Users were bouncing before the page even loaded.

Optimization #1: Code Splitting

Problem: Shipping one massive JavaScript bundle.

Solution: Split code by route:

// Before: Everything in one bundle import Dashboard from './pages/Dashboard' import Profile from './pages/Profile' import Settings from './pages/Settings' // After: Lazy load route components const Dashboard = lazy(() => import('./pages/Dashboard')) const Profile = lazy(() => import('./pages/Profile')) const Settings = lazy(() => import('./pages/Settings')) function App() { return ( <Suspense fallback={<PageLoader />}> <Routes> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/profile" element={<Profile />} /> <Route path="/settings" element={<Settings />} /> </Routes> </Suspense> ) }

Result: Initial bundle dropped from 847KB to 234KB (-72%)

Optimization #2: Memoization

Problem: Expensive calculations running on every render.

useMemo for Expensive Computations

function DestinationList({ destinations, filters }) { // BAD: Recalculates on every render const filtered = destinations .filter(d => d.price <= filters.maxPrice) .sort((a, b) => b.rating - a.rating) // GOOD: Only recalculates when dependencies change const filteredDestinations = useMemo(() => { return destinations .filter(d => d.price <= filters.maxPrice) .sort((a, b) => b.rating - a.rating) }, [destinations, filters.maxPrice]) return <List items={filteredDestinations} /> }

React.memo for Component Memoization

// Expensive component that shouldn't re-render unnecessarily const DestinationCard = React.memo(({ destination, onBook }) => { return ( <Card> <Image src={destination.image} /> <Title>{destination.name}</Title> <Price>${destination.price}</Price> <Button onClick={() => onBook(destination.id)}> Book Now </Button> </Card> ) }, (prevProps, nextProps) => { // Custom comparison function return prevProps.destination.id === nextProps.destination.id })

useCallback for Event Handlers

function BookingForm() { const [bookings, setBookings] = useState([]) // BAD: Creates new function on every render const handleBook = (id) => { setBookings(prev => [...prev, id]) } // GOOD: Function reference stays stable const handleBook = useCallback((id) => { setBookings(prev => [...prev, id]) }, []) return ( <DestinationList onBook={handleBook} /> ) }

Optimization #3: Virtualization

Problem: Rendering 1000+ items in a list kills performance.

Solution: React Window (virtualization):

import { FixedSizeList } from 'react-window' function DestinationList({ destinations }) { const Row = ({ index, style }) => ( <div style={style}> <DestinationCard destination={destinations[index]} /> </div> ) return ( <FixedSizeList height={600} itemCount={destinations.length} itemSize={200} width="100%" > {Row} </FixedSizeList> ) }

Result: Render time for 1000 items: 2400ms → 45ms (98% faster!)

Optimization #4: Image Optimization

Problem: Shipping huge images (2MB+ per image).

Solution: Next.js Image component + modern formats:

import Image from 'next/image' // Before: Regular img tag <img src="/paris.jpg" alt="Paris" /> // Size: 2.3MB, format: JPEG // After: Optimized Next.js Image <Image src="/paris.jpg" alt="Paris" width={800} height={600} placeholder="blur" blurDataURL="data:image/jpeg;base64,..." quality={85} formats={['image/avif', 'image/webp']} /> // Size: 47KB, format: AVIF

Result: 98% reduction in image payload!

Optimization #5: Debouncing Search

Problem: API call on every keystroke.

import { useDebouncedCallback } from 'use-debounce' function SearchBar() { const [results, setResults] = useState([]) const handleSearch = useDebouncedCallback( async (query) => { const data = await fetch(`/api/search?q=${query}`) setResults(data) }, 500 // Wait 500ms after user stops typing ) return ( <input onChange={(e) => handleSearch(e.target.value)} placeholder="Search destinations..." /> ) }

Result: Reduced API calls by 95%

Optimization #6: Prefetching

Load data before user clicks:

import { useEffect } from 'react' import { useRouter } from 'next/router' function DestinationCard({ destination }) { const router = useRouter() const handleMouseEnter = () => { // Prefetch the destination page router.prefetch(`/destinations/${destination.id}`) } return ( <Card onMouseEnter={handleMouseEnter}> <Link href={`/destinations/${destination.id}`}> <Image src={destination.image} /> <Title>{destination.name}</Title> </Link> </Card> ) }

Result: Near-instant page transitions!

Optimization #7: Bundle Analysis

Find what's bloating your bundle:

# Install bundle analyzer npm install --save-dev @next/bundle-analyzer # Add to next.config.js const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true', }) module.exports = withBundleAnalyzer({ // your config }) # Run analysis ANALYZE=true npm run build

Findings:

  • Moment.js (300KB) → Replaced with date-fns (12KB)
  • Lodash (71KB) → Use individual imports
  • Unused dependencies removed

Result: Another 350KB saved!

Optimization #8: Lighthouse CI

Automate performance testing:

# .github/workflows/lighthouse.yml name: Lighthouse CI on: [pull_request] jobs: lighthouse: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Run Lighthouse CI uses: treosh/lighthouse-ci-action@v8 with: urls: | https://staging.example.com uploadArtifacts: true

Never ship slow code again!

Final Results

MetricBeforeAfterImprovement
FCP2.8s0.4s-86%
TTI4.1s0.9s-78%
Bundle847KB234KB-72%
Lighthouse6296+55%

The Checklist

  • Code split by route
  • Memoize expensive calculations
  • Virtualize long lists
  • Optimize images (AVIF/WebP)
  • Debounce user input
  • Prefetch likely navigations
  • Analyze bundle size
  • Remove unused dependencies
  • Lazy load below-the-fold content
  • Enable compression (Gzip/Brotli)

Tools I Use

  1. Chrome DevTools - Performance profiling
  2. React DevTools Profiler - Component render times
  3. Lighthouse - Overall performance score
  4. Bundle Phobia - Check package sizes before installing
  5. Web Vitals - Real user metrics

Common Mistakes to Avoid

  1. ❌ Using index as key in lists
  2. ❌ Inline function definitions in JSX
  3. ❌ Not memoizing context values
  4. ❌ Forgetting to cleanup useEffect
  5. ❌ Overusing state (derive when possible)

Conclusion

Performance isn't a feature - it's a requirement. Users expect fast, smooth experiences. These optimizations took my app from "barely usable" to "blazing fast."

Start with code splitting and image optimization - you'll see the biggest wins there.


Want to see these techniques in action? Check out my portfolio projects or connect on LinkedIn.

Further Reading:

Let's Build Something Great

I'm open to freelance, full-time, or collaboration opportunities.