Our app was embarrassingly slow.
13.5 seconds to First Contentful Paint on mobile. A Lighthouse score of 47. Users were waiting — and probably leaving.
I'd just joined as the frontend engineer at a SaaS startup. Nobody told me to fix the performance. But the numbers were bad enough that I couldn't ignore them.
Here's exactly what I did, and what I learned.
The Starting Point (It Was Bad)
Before I touched anything:
| Metric | Score |
|---|---|
| JS Bundle | 2,054 KB (~2MB) |
| CSS | 175 KB |
| First Contentful Paint | 13.5 seconds |
| Lighthouse Mobile | 47 / 100 |
| Lighthouse Desktop | 60 / 100 |
A 2MB JavaScript bundle. On mobile. In 2025.
This wasn't a slow app — it was a broken experience. Nobody waits 13 seconds for a dashboard to load.
Finding the Culprit
First step: understand why it's this bad. I ran a bundle analysis and found three main problems.
1. Everything was loading upfront
The app had no code splitting. Every route, every modal, every component — all of it loaded on the very first page visit. Users were downloading code for screens they'd never even open.
2. A massive unused script: gptengineer.js
This was the biggest single win. Someone had added a heavy script from an AI prototyping tool during early development and it never got removed. It was sitting in the bundle, loading on every page, doing absolutely nothing in production.
This single file was responsible for a significant chunk of that 2MB.
3. CSS bloat
175KB of CSS — most of it unused. Styles imported globally, never cleaned up, just accumulating.
The Fixes (Nothing Exotic)
I want to be honest: I didn't use any fancy techniques here. These are standard practices. The problem was they just hadn't been applied yet.
Fix 1: Lazy loading routes and heavy components
// Before — loads everything immediately
import Dashboard from './pages/Dashboard'
import CampaignManager from './pages/CampaignManager'
import Analytics from './pages/Analytics'
// After — loads only when needed
const Dashboard = lazy(() => import('./pages/Dashboard'))
const CampaignManager = lazy(() => import('./pages/CampaignManager'))
const Analytics = lazy(() => import('./pages/Analytics'))
Wrap with <Suspense> and you're done. React does the heavy lifting — it splits each lazy import into its own chunk and only fetches it when the user navigates to that route.
The impact was immediate. The initial bundle dropped significantly because users on the login page no longer needed to download the entire dashboard.
Fix 2: Remove dead code ruthlessly
gptengineer.js — gone. It was one line to remove it. That one line recovered hundreds of KB.
This is a lesson I'll carry forward: audit your dependencies regularly. Dev tools leave artifacts. Prototyping libraries get committed. Nobody notices until someone runs a bundle analysis.
# Run this. Look at what's in your bundle.
npx vite-bundle-visualizer
# or for webpack:
npx webpack-bundle-analyzer
Fix 3: Clean up global CSS imports
Instead of importing everything globally, I scoped styles to where they were actually used and removed entire CSS files that had no live references in the codebase.
This took the CSS from 175KB → 40KB. A 77% reduction just from cleanup.
The Results
After three focused days of work:
| Metric | Before | After | Change |
|---|---|---|---|
| JS Bundle | 2,054 KB | 359 KB | 83% smaller |
| CSS | 175 KB | 40 KB | 77% smaller |
| First Contentful Paint | 13.5s | 4.4s | 3x faster |
| Lighthouse Mobile | 47 | 68 | +21 points |
| Lighthouse Desktop | 60 | 96 | +36 points |
Desktop went from 60 to 96. Mobile from 47 to 68.
The app felt like a different product.
What I'd Tell My Past Self
Measure first, always. I knew the app was slow but I didn't know why until I ran the bundle analysis. Guessing wastes time.
The basics compound. Code splitting + removing dead code + cleaning CSS. Nothing groundbreaking. But each one reinforced the others.
Performance is everyone's job. It took me joining the team for this to get fixed. That's too late. Bundle audits should be part of regular dev cycles — not something discovered by accident.
The 2MB was embarrassing in hindsight, but that's fine. Fast-moving startups ship features first and optimize later. The key is knowing when "later" has arrived.
Quick Checklist if You're in the Same Situation
- [ ] Run a bundle visualizer — find what's actually in there
- [ ] Search for dev/prototyping tools left in production (
gptengineer,lovable, etc.) - [ ] Add
React.lazy()+Suspensefor route-level code splitting - [ ] Check for globally imported CSS with no live references
- [ ] Set a bundle size budget in your build config so this doesn't creep back
If you've got a React app that feels sluggish and haven't run a bundle analysis yet — do it today. You might be surprised what you find.
Happy to answer questions in the comments. What's the worst bundle size you've seen in the wild?
Top comments (0)