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.
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.
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.