Skip to main content

Command Palette

Search for a command to run...

Breaking Down a 68MB React Build: Architecture Fixes That Cut It to 21MB

Published
5 min read
Breaking Down a 68MB React Build: Architecture Fixes That Cut It to 21MB

Our React production build was 68MB.
Not a typo. Sixty. Eight. MB.

Deployments were slow. Uploads were painful. And the easy excuse was:
“Big app. Happens.”

Nah.

So I opened the bundle and started pulling it apart.

This post walks through:

  • How I analyze a production build when something feels off

  • What usually hides inside large React bundles

  • The architectural fixes that brought it down from 68MB to 21MB

Because this isn’t about shaving numbers for fun.
It’s about faster deploys, better caching, quicker rollbacks — and not panicking on release day.

If you're shipping frontend to production, this stuff matters.


Step 1 — Quick Win: Kill Source Maps in Production

First check: production was generating source maps.

Turned that off.

68MB → 25MB instantly.

Good win.
But that still left 25MB of actual application code.

So the real question became:

What exactly are we shipping to users?


Step 2 — What Are We Actually Shipping?

A production bundle is basically a giant, minified blob.

Manually scanning files isn’t realistic.
So I generated build stats and ran them through a Webpack bundle analyzer.

The treemap visualization made it very obvious where the weight was coming from.

Webpack Bundle Analyzer treemap showing Highcharts and other heavy dependencies inside the main chunk

In a clean, role-based SaaS architecture:

  • The main bundle should contain only core app logic

  • Feature libraries should load with their routes

  • Heavy libraries (charts, PDF tools, editors, etc.) should not sit in the entry chunk unless truly global

The visualization made one thing clear — that separation wasn’t strict.

And that’s where the real fixes started.


Step 3 — Keep Route-Level Dependencies Out of the Main Bundle

While reviewing the treemap, one thing stood out.

Highcharts was inside the main bundle.

That didn’t make sense.

Charts were used only inside dashboards.
Dashboards were lazy-loaded per role.

After tracing imports, I found this in the root entry file:

import Highcharts from "highcharts";

Highcharts.setOptions({
  chart: {
    style: {
      fontFamily: "'Poppins', sans-serif",
    },
  },
});

That one import was enough.

Even though routes were lazy-loaded, the dependency wasn’t.

So every single user — even those who never opened a dashboard — was downloading a charting library.

Fix

I moved the chart configuration into a dedicated chart module and imported it only inside chart-related components.

Rebuilt.

25MB → 21MB.

React Webpack Bundle Analyzer treemap after removing Highcharts from main bundle

More importantly:

  • Highcharts was removed from the main bundle

  • It now loads only when a dashboard loads

  • Route-based code splitting started behaving correctly

This wasn’t just about shaving 4MB.

It was about making sure users download only what they actually use.


Step 4 — Don’t Ship Two Libraries for One Problem

During dependency audit, both moment and dayjs were present.

Classic enterprise pattern.

Features get added. Libraries stick around.

But duplicate libraries mean:

  • Bigger bundle

  • More cognitive load

  • More maintenance surface area

Before removing anything, I evaluated:

  • Actual feature usage in the codebase

  • Edge cases (timezones, parsing, formatting)

  • Bundle size impact using tools like Bundlephobia

dayjs covered our requirements with a smaller footprint.

So I standardized on dayjs, replaced remaining moment usage, and removed it.

Result:

  • Leaner dependency graph

  • Smaller production build

  • Cleaner architectural boundary

Optimization isn’t always about clever tricks.

Sometimes it’s about deliberate decisions.


Step 5 — Static Assets Matter Too

JavaScript isn’t the only thing that grows silently.

Over time, the /public folder had accumulated unused images and static assets — old design iterations, deprecated illustrations, leftover icons.

These don’t show up in JS bundle analyzers, but they:

  • Increase deployment size

  • Slow down uploads

  • Add unnecessary storage and CDN overhead

A quick audit removed unused files and cleaned up the directory structure.

Performance isn’t just about code.

It’s about everything being shipped.


What I Look For in Enterprise Bundle Optimization

From this experience, these are the patterns I now actively check:

1. Entry Point Imports

Anything imported in index.tsx or root files becomes part of the main bundle.

Global configuration imports must be intentional.

2. Feature Isolation

Route-based apps should reflect in bundle structure.

If dashboards are lazy-loaded, their heavy dependencies must be lazy-loaded too.

3. Duplicate Libraries

Multiple libraries solving the same problem should be evaluated and standardized.

4. Heavy Libraries

Charts, PDF tools, editors, icon packs — common bloat sources.

They should be lazy-loaded, dynamically imported, or replaced with lighter alternatives.

5. Raw Code vs Dependency

Sometimes small utilities are better implemented in-house instead of importing an entire package.

Dependency discipline matters in enterprise systems.


Final Results

Metric Before After
Production Build 68MB 21MB
Minified Main Bundle 529.57 KB 431.56 KB
Main Bundle Content Included route-level chart library Clean core bundle
Date Libraries Two One
Deployment Time Slower Improved

Key Learnings

  • Bundle size reflects architecture decisions.

  • Lazy loading works only when dependencies are isolated correctly.

  • Root-level imports can silently defeat code splitting.

  • Regular dependency audits are essential in growing codebases.

  • Optimization isn’t about removing libraries — it’s about structuring intentionally.

This experience changed how I think about performance in React systems.

Instead of optimizing at the end, bundle structure is now treated as part of architecture design from day one.