Performance isn't just a metric—it's a user experience. When we audited our main SaaS dashboard last quarter, we found something alarming: our initial JavaScript bundle was sitting at 2.8MB, with a parsed size of over 1.2MB. Load times were sluggish on 3G networks, and our Core Web Vitals scores were barely passing.
We knew we had to act. What followed was a three-week deep dive into build configuration, dependency auditing, code splitting strategies, and asset optimization. The result? A 40% reduction in bundle size, faster TTFB, and a 1.8s improvement in Largest Contentful Paint.
Here's exactly how we did it, the tools we used, and the architectural changes that made the biggest impact.
1. The Investigation: Finding the Culprits
Before optimizing, you need visibility. We started by generating a production build and visualizing the dependency tree using webpack-bundle-analyzer.
npx webpack-bundle-analyzer dist/stats.json
The visualization revealed three major bottlenecks:
- Legacy dependencies: Full imports of
lodash,moment, andthree.jswere inflating the bundle unnecessarily. - No route-based splitting: Every component was bundled into
main.js, regardless of when or where it was used. - Unoptimized assets: Inline SVGs, unminified JSON configs, and heavy base64-encoded images were being shipped upfront.
2. Dependency Audit & Tree-Shaking
Modern bundlers support tree-shaking, but it only works if dependencies export pure ES modules and consumers use named imports. We replaced heavy libraries with lighter, tree-shakeable alternatives:
// Before
"lodash": "^4.17.21",
"moment": "^2.29.4"
// After
"lodash-es": "^4.17.21",
"date-fns": "^3.0.0"
We also audited usage patterns. Functions like _.debounce and _.cloneDeep were being imported globally. Switching to named imports reduced dead code by ~320KB alone.
"If you're importing from a library, import only what you use. Modern bundlers will drop the rest—but only if you ask them to."
3. Code Splitting & Lazy Loading
Our app had over 40 routes, but 90% of them were only accessed occasionally. We implemented route-based code splitting using React.lazy and Route components:
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./routes/Dashboard'));
const Analytics = lazy(() => import('./routes/Analytics'));
const Settings = lazy(() => import('./routes/Settings'));
export default function AppRouter() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
This alone dropped our initial JS payload from 2.8MB to 1.4MB. Non-critical routes are only fetched when the user navigates to them.
4. Asset & Media Optimization
JavaScript wasn't the only culprit. We found:
- SVG icons were being imported individually as base64 strings
- Hero images were uncompressed PNGs
- Font files included unused character sets
We migrated to an SVG sprite sheet, converted all images to WebP/AVIF with fallbacks, and subsetted our fonts to only include Latin and Cyrillic glyphs. Asset sizes dropped by 65%.
5. Build Configuration & Compression
We upgraded from Webpack to Vite + Rollup for our client build pipeline. ESBuild's native CJS/ESM handling and aggressive minification shaved off another 18%.
On the infra side, we enabled Brotli compression via our CDN. While this doesn't reduce bundle size at the source, it cuts network transfer by ~30-40% for text assets.
The Results
After two weeks of iterative optimization, CI/CD validation, and QA testing, here's what we achieved:
The impact was immediate. Bounce rates on low-end devices dropped by 22%, and user session duration increased by 14%. Most importantly, our app now feels instantaneous on first load.
Key Takeaways
- Measure first, optimize second. Blind optimization wastes time. Use bundle analyzers to find real bottlenecks.
- Lazy load ruthlessly. Ship only what's needed for the initial render. Let the browser fetch the rest on demand.
- Audit dependencies quarterly. Packages accumulate dead weight over time. Replace heavy libs with modern, tree-shakeable alternatives.
- Compress at every layer. Source minification, brotli/gzip, and CDN caching work together to maximize speed.
What's Next?
We're now implementing module federation to share components across micro-frontends without duplication, and exploring React Server Components to push more rendering to the edge. Bundle size optimization isn't a one-time fix—it's a continuous discipline.
If you're dealing with bloated bundles or struggling with Core Web Vitals, don't wait. Start analyzing, start splitting, and measure everything. Your users (and your Lighthouse score) will thank you.
Did this help your build pipeline? Share your optimization wins with us on Twitter or drop a comment below. We read every one.