React Performance Optimization: From 3s to 300ms Load Time
January 22, 2025
9 min read
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
| Metric | Before | After | Improvement |
|---|---|---|---|
| FCP | 2.8s | 0.4s | -86% |
| TTI | 4.1s | 0.9s | -78% |
| Bundle | 847KB | 234KB | -72% |
| Lighthouse | 62 | 96 | +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
- Chrome DevTools - Performance profiling
- React DevTools Profiler - Component render times
- Lighthouse - Overall performance score
- Bundle Phobia - Check package sizes before installing
- Web Vitals - Real user metrics
Common Mistakes to Avoid
- ❌ Using
indexas key in lists - ❌ Inline function definitions in JSX
- ❌ Not memoizing context values
- ❌ Forgetting to cleanup useEffect
- ❌ 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.