The 10MB JavaScript Bundle That Nobody Noticed

August 7, 2026

Tech Stack: React, Vite, TypeScript, SCSS, Lighthouse, Chrome DevTools, Brotli, Cloud CDN

Sometimes the most valuable engineering work doesn't come from a Jira ticket.

It comes from noticing a problem that everyone else has learned to live with.

That happened to me while working on our company's main dashboard.

Whenever I visited my hometown, the application felt painfully slow.

It wasn't unusable.

But it took far longer than it should have before I could actually start interacting with it.

Interestingly, nobody had ever reported it.

After thinking about it for a while, the reason became obvious.

Our product is B2B.

Most customers use the application from offices with stable internet connections.

The issue simply never became important enough for anyone to investigate.

But that didn't mean it wasn't worth solving.

As engineers, I believe we shouldn't wait for every improvement to become a ticket.

Sometimes it's our responsibility to identify problems before customers do.

Measuring Before Optimizing

Rather than guessing, I opened Chrome DevTools and Lighthouse.

The very first thing that caught my attention was shocking.

Initial JavaScript downloaded ≈ 10 MB

That immediately answered why the application felt slow on weaker networks.

But it raised a much more interesting question.

Why was it so large?

There Wasn't One Problem

There were dozens.

None of them looked catastrophic individually.

Together, they created a terrible loading experience.

1. Fonts Were Blocking Rendering

We were loading custom fonts synchronously.

That meant the browser couldn't continue rendering until font requests completed.

Instead of this:

<link rel="stylesheet" href="fonts.css" />

We switched to a non-blocking strategy and only loaded fonts when necessary.

Lighthouse immediately reported improvements in the critical rendering path.

2. Third-Party Libraries Loaded Immediately

Analytics.

Logging.

Monitoring.

Everything loaded during startup.

Even before the user interacted with the application.

Libraries like these don't need to delay first paint.

Moving them out of the critical path immediately reduced startup work.

3. Everything Lived in One Giant Vendor Bundle

At that time our Vite configuration was almost the default configuration.

That meant unrelated dependencies ended up inside the same chunk.

Charts.

React.

Date libraries.

Utility packages.

Everything.

We reorganized the build using manual chunking.

manualChunks: { react: [...], charts: [...], vendor: [...], ui: [...], }

Now browsers could cache each dependency independently.

Updating one package no longer invalidated the entire vendor bundle.

4. Tree Shaking Wasn't Actually Working

One of the biggest surprises came from our imports.

Things like:

import * as Icons from "@company/icons";

or barrel exports like:

export * from "./utils";

prevented effective tree shaking.

After switching to direct imports:

import SearchIcon from "./SearchIcon";

the build became noticeably smaller.

5. Some Modules Were Lazy Loaded...

...and others weren't.

There wasn't any clear loading strategy.

Some large routes were lazy.

Others imported everything eagerly.

We revisited the routing structure and loaded features only when users actually needed them.

6. Internal Libraries Were Shipping More Than They Should

Some shared libraries bundled:

  • fonts
  • images
  • dependencies already installed by the application

Those assets were duplicated inside the final bundle.

Externalizing dependencies and moving shared assets out of those packages reduced unnecessary downloads.

7. CSS Had Quietly Grown

Over the years, duplicate SCSS, unused styles, and repeated utility classes accumulated across the project.

Nothing dramatic.

Just years of incremental growth.

Cleaning them up reduced stylesheet size and improved maintainability.

8. Compression Wasn't Enabled

One of the easiest wins wasn't even inside the frontend.

The server wasn't serving Brotli-compressed assets.

I worked with our cloud team to enable:

  • Brotli compression
  • Proper cache headers
  • Long-term caching for hashed assets

Sometimes the biggest frontend optimizations don't happen inside React.

9. Build Hygiene Matters

Performance isn't only about Lighthouse scores.

It's also about maintaining a healthy build.

Along the way I also cleaned up:

  • circular imports
  • inconsistent lazy loading
  • import hierarchy
  • oversized modules
  • duplicated constants
  • JSX mixed with configuration files
  • unnecessary package dependencies

None of these changes alone were revolutionary.

Together they made the codebase significantly healthier.

The Result

After addressing dozens of bottlenecks across the application, build process, and delivery pipeline, the improvements were significant.

MetricBeforeAfter
Initial JavaScript Bundle~10 MB1.3 MB
Initial Download (with Brotli)~10 MB~500 KB
Lighthouse Performance4783
Best Practices8496
Accessibility9595
Initial Load Time (Slow Network)~80% faster

Reducing the bundle size wasn't the only goal.

The application became noticeably faster on slower networks, interactive much sooner, and significantly more cache-friendly. More importantly, the improvements weren't tied to one optimization—they came from systematically removing bottlenecks across the entire loading pipeline.

Lessons Learned

The biggest lesson wasn't about Vite.

Or Lighthouse.

Or React.

It was this:

Performance is rarely one big problem. It's usually hundreds of small decisions accumulated over years.

My Performance Checklist

Whenever I work on an existing frontend application, these are the first things I investigate.

Measure First

  • Profile using Chrome DevTools and Lighthouse.
  • Identify what blocks the critical rendering path.
  • Never optimize based on assumptions.

JavaScript

  • Audit bundle size.
  • Verify tree shaking is actually working.
  • Replace wildcard (*) imports with direct imports.
  • Remove unnecessary barrel exports where they prevent tree shaking.
  • Lazy load routes and heavy features.
  • Split vendor chunks for better browser caching.

CSS

  • Remove duplicate or unused styles.
  • Avoid importing large global styles unnecessarily.
  • Keep component styles close to where they're used.

Assets

  • Load fonts asynchronously.
  • Compress and optimize images.
  • Preload only truly critical assets.

Third-Party Libraries

  • Delay analytics, logging, and monitoring until after the initial render.
  • Audit whether every dependency is actually needed.
  • Externalize shared dependencies inside internal packages.

Build

  • Eliminate circular dependencies.
  • Keep import hierarchy clean.
  • Separate configuration, constants, and JSX to improve tree shaking.
  • Organize chunks intentionally instead of relying entirely on default bundling.

Network

  • Enable Brotli compression.
  • Configure long-term cache headers for hashed assets.
  • Verify CDN caching behavior.

Finally

Measure again.

Every optimization should have measurable impact.

Most applications don't need one revolutionary optimization.

They need twenty good ones.

And that's usually enough to transform the user experience.

GitHub
LinkedIn