Performance Frontend Webpack Core Web Vitals

How We Reduced Our Bundle Size by 40%

Oct 15, 2025 12 min read Engineering
DR
Daniel Reyes Senior Frontend Engineer

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.

Terminal
npx webpack-bundle-analyzer dist/stats.json

The visualization revealed three major bottlenecks:

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:

package.json (dependencies)
// 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:

App.jsx
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:

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:

Initial JS Bundle
1.68 MB
↓ 40% reduction
Largest Contentful Paint
2.4s
↓ 1.8s faster
Total Blocking Time
80ms
↓ 62% improvement
Lighthouse Performance
94
↑ from 68

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

  1. Measure first, optimize second. Blind optimization wastes time. Use bundle analyzers to find real bottlenecks.
  2. Lazy load ruthlessly. Ship only what's needed for the initial render. Let the browser fetch the rest on demand.
  3. Audit dependencies quarterly. Packages accumulate dead weight over time. Replace heavy libs with modern, tree-shakeable alternatives.
  4. 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.