React Performance Optimization: From 3s to 300ms Load Time
π Updated: July 29, 2026π Web Developmentβ±οΈ 14 min readβ Real Case Study
Last month, I inherited a React app that took 3.2 seconds to load. Users were bouncing, Core Web Vitals were in the red, and the client was not happy. After two weeks of optimization, I got it down to 300msβa 10x improvement.
In this guide, I'll walk you through exactly what I did, with before/after metrics and code examples you can use in your own projects.
π Results Summary
Load Time
300ms
β 90% from 3.2s
Bundle Size
180KB
β 85% from 1.2MB
LCP
1.2s
β 70% from 4.1s
TTI
1.8s
β 75% from 7.2s
The Starting Point: What Was Wrong
Before diving into solutions, let's understand the problems. I ran Lighthouse and WebPageTest to identify bottlenecks:
Massive bundle size: 1.2MB of JavaScript (gzipped)
No code splitting: Everything loaded at once
Unoptimized images: 5MB of images on homepage
Render-blocking resources: CSS and fonts blocking render
Poor caching: No cache headers, everything re-downloaded
Unused dependencies: Moment.js, Lodash (full), and more
Step 1: Analyze Your Bundle
1 Find What's Making Your Bundle Huge
Before optimizing, you need to know what's in your bundle.
Install Bundle Analyzer
# For Create React App
npm install --save-dev webpack-bundle-analyzer
# Add to package.json scripts:
"analyze": "source-map-explorer 'build/static/js/*.js'"
# For Vite
npm install --save-dev rollup-plugin-visualizer
# Add to vite.config.js:
import { visualizer } from 'rollup-plugin-visualizer';
plugins: [visualizer({ open: true })]
What I Found
Running the analyzer revealed the culprits:
Moment.js: 300KB (with locales) - only used for date formatting
Lodash: 70KB - importing the entire library
React-Quill: 150KB - rich text editor on every page
Chart.js: 200KB - charts only on dashboard
Unused components: 100KB of dead code
π‘ Pro Tip
Run bundle analysis on every PR. Add it to your CI/CD pipeline to catch bloat early.
Step 2: Replace Heavy Dependencies
2 Swap Bloated Libraries for Lightweight Alternatives
Most heavy libraries have modern, smaller alternatives.
Moment.js β date-fns
// Before: 300KB
import moment from 'moment';
const formatted = moment(date).format('MMM DD, YYYY');
// After: 2KB (tree-shakeable)
import { format } from 'date-fns';
const formatted = format(date, 'MMM dd, yyyy');
Savings: 298KB (99% reduction)
Lodash β Native Methods
// Before: 70KB (full library)
import _ from 'lodash';
const result = _.debounce(fn, 300);
const unique = _.uniq(array);
// After: 0KB (native)
// Debounce: write your own (10 lines)
function debounce(fn, delay) {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
}
// Unique: use Set
const unique = [...new Set(array)];
Savings: 70KB (100% reduction)
React-Quill β React-Quill (Lazy Loaded)
// Before: 150KB loaded on every page
import ReactQuill from 'react-quill';
// After: 150KB only loaded when needed
import { lazy, Suspense } from 'react';
const ReactQuill = lazy(() => import('react-quill'));
function Editor() {
return (
Loading editor...}>
);
}
Savings: 150KB on initial load (loaded only when user opens editor)
Step 3: Implement Code Splitting
3 Load Only What You Need, When You Need It
Code splitting is the single most impactful optimization for most React apps.
Route-Based Splitting
// Before: All routes in one bundle
import Home from './pages/Home';
import Dashboard from './pages/Dashboard';
import Settings from './pages/Settings';
function App() {
return (
} />
} />
} />
);
}
// After: Each route is a separate chunk
import { lazy, Suspense } from 'react';
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
Loading...}>
} />
} />
} />
);
}
Impact: Initial bundle reduced from 1.2MB to 180KB (only Home page loaded)
Component-Based Splitting
// Heavy component only loaded when needed
const HeavyChart = lazy(() => import('./components/HeavyChart'));
function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
{showChart && (
Loading chart...
}>
)}
);
}
Step 4: Optimize Images
4 Images Are Usually the Biggest Culprit
Images often account for 50-80% of page weight. Optimize them aggressively.
Use Modern Formats
// Before: 5MB of JPEGs and PNGs
// After: 500KB of WebP/AVIF with fallbacks
Impact: Reduced image weight from 5MB to 500KB, and only load images as user scrolls
Step 5: Optimize Rendering
5 Prevent Unnecessary Re-renders
Even with a small bundle, poor rendering performance can make your app feel slow.
Use React.memo for Expensive Components
// Before: Re-renders on every parent update
function ExpensiveList({ items }) {
return (
{items.map(item => (
{item.name}
))}
);
}
// After: Only re-renders when items change
const ExpensiveList = React.memo(function ExpensiveList({ items }) {
return (
{items.map(item => (
{item.name}
))}
);
});
Use useMemo and useCallback
// Before: Expensive calculation runs on every render
function Dashboard({ data }) {
const processedData = data.map(item => {
// Expensive operation
return complexTransformation(item);
});
return ;
}
// After: Calculation only runs when data changes
function Dashboard({ data }) {
const processedData = useMemo(() => {
return data.map(item => complexTransformation(item));
}, [data]);
return ;
}
β οΈ Don't Over-Optimize
Only use memo, useMemo, and useCallback when you've measured a performance problem. Premature optimization can make code harder to read without meaningful gains.
Step 6: Implement Caching
6 Cache Everything You Can
Proper caching can reduce load times by 50-80% for returning users.
HTTP Cache Headers
// In your server or CDN config
// Static assets (JS, CSS, images) - cache for 1 year
Cache-Control: public, max-age=31536000, immutable
// HTML - cache for 5 minutes, revalidate
Cache-Control: public, max-age=300, must-revalidate
// API responses - cache based on data freshness
Cache-Control: private, max-age=60
Service Worker for Offline Support
// Install Workbox
npm install workbox-webpack-plugin
// In your service worker
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate } from 'workbox-strategies';
// Precache app shell
precacheAndRoute(self.__WB_MANIFEST);
// Cache API responses
registerRoute(
({url}) => url.pathname.startsWith('/api/'),
new StaleWhileRevalidate({
cacheName: 'api-cache',
plugins: [
new ExpirationPlugin({
maxEntries: 50,
maxAgeSeconds: 5 * 60, // 5 minutes
}),
],
})
);
Impact: Returning users load the app in <100ms (from cache)
Step 7: Optimize Critical Rendering Path
7 Get Content on Screen Faster
Optimize what the browser needs to render the first meaningful paint.
The result? A 10x faster app, happier users, and better Core Web Vitals. The best part: most of these optimizations are one-time changes that keep paying off.
π‘ Final Tip
Don't try to implement everything at once. Start with code splitting and dependency replacementβthey'll give you 80% of the gains with 20% of the effort.
W
Wivrix Team
We write practical guides based on real-world experience. No theory, just proven techniques.