Skip to content
abdulmazid.tech

Cutting a React bundle by 68%, and why the precache was the real problem

A client site shipped 1.18 MB of JavaScript and precached 65 MB of assets to every visitor. The bundle got the attention; the service worker was doing far more damage.

Abdul Mazid6 min read

A client came to me with "the site feels slow". No numbers, no baseline — which is the normal starting point, and the first job is always to replace the feeling with a measurement.

Here is what the audit found, what fixed it, and the one that surprised me.

Measure first, and write it down

Before changing anything: record the current numbers. Not because it is good practice in the abstract, but because without a baseline you cannot tell the difference between a fix and a change.

The starting position:

Main JavaScript bundle 1,177.94 kB
Main bundle, gzipped 357.68 kB
Service worker precache 66,545.83 KiB
Homepage hero image 748.56 kB PNG

That third row is not a typo. Sixty-five megabytes.

The bundle was the obvious problem

1.18 MB of JavaScript before a single pixel renders. On a mid-range phone on a mobile network, that is several seconds during which the user sees nothing.

The fixes here are unglamorous and well documented: split routes so a visitor to the homepage does not download the admin dashboard, import individual functions rather than whole utility libraries, and lazily load the components that only appear after interaction.

Result: 1,177.94 kB → 371.90 kB, a 68% reduction. Gzipped, 357.68 kB → 123.99 kB.

Good. But not the biggest win.

The precache was the real problem

The site was a Progressive Web App. The service worker's precache glob had been left broad enough to match essentially every asset in the build output — including the full-resolution source images for the gallery.

The consequence: every first-time visitor's browser was instructed to download 65 megabytes in the background. On a metered mobile connection, this is not merely slow — it is actively costing the visitor money to look at a roofing company's website.

Narrowing the precache to what is genuinely needed for offline use — the shell, the critical CSS, the offline fallback page — took it to 562.75 KiB. A 99% reduction.

The lesson I keep relearning: the most expensive thing on a site is usually not the thing everyone is looking at. Bundle size is what gets discussed because it is what the build output prints. The precache was ten times more damaging and completely silent.

The images

748.56 kB for a hero PNG, served at full resolution regardless of viewport.

Converting to WebP at appropriate dimensions took it to 103.17 kB — 86% smaller — with no visible difference at any screen size.

This is the least interesting fix and it is available on almost every site I audit.

What the crawler was seeing

While measuring, something worse turned up. The site was a client-rendered single-page app, so every URL returned the same near-empty HTML shell and the content only existed after JavaScript executed.

Worse:

  • Unknown URLs returned HTTP 200 instead of 404, inviting search engines to index pages that did not exist
  • Both www and the apex domain returned 200, splitting authority across two hostnames
  • The login and dashboard routes were indexable

And the one that made me stop: the production build was sending API requests to localhost:5000 — every visitor's own machine. It had presumably been failing quietly for a long time, appearing as "the contact form is sometimes flaky".

The fix was a prerender step: generate real HTML for all 18 routes at build time, each with its own title, canonical URL, H1 and structured data, so a crawler gets a complete document on first response. Then real 404s for unknown URLs, a permanent redirect from www to the apex, and X-Robots-Tag: noindex on the admin routes.

What I did not claim

The client asked for a Lighthouse score to put on their marketing.

I did not give them one, because at that point a meaningful production Lighthouse run required the updated build and the new server configuration to actually be deployed. Quoting a local score as a production result is how "100/100" ends up on a site that is nothing of the sort.

The bundle numbers above are real, measured, and dated. That is worth more than a score, because every one of them can be independently verified by running the build.

The order that mattered

  1. Measure and record. Otherwise nothing afterwards is provable.
  2. Look at what is silent. The precache was invisible in every build report and was the single biggest cost.
  3. Check what a crawler sees, not what a browser sees. They are different documents.
  4. Only claim what you measured.

That third one is where the localhost bug surfaced. It had nothing to do with performance. It was found because "view the page the way a crawler does" is a different question from "does the site work on my machine" — and asking it is how you find the things nobody has thought to look for.

  • performance
  • react
  • core-web-vitals
  • seo

Get the next one by email

Occasional, and only when there is something worth sending.

Keep reading

Running a studio

Stripe does not work in Bangladesh. Here is what actually does.

Stripe, PayPal and Wise are all unavailable to a Bangladeshi merchant, and the popular merchant-of-record platforms ban selling services outright. A practical map of what is left, and what it means for how you architect billing.

7 min read