I Weighed My Own Site in Production: 1.8 MB of JavaScript on Every Tool Page
I measured every page I ship: HTML is tiny once gzipped, but tool pages load 1.8 MB of JavaScript — including a PDF engine on the JSON formatter. Here are the real numbers and the fix.
Everyone who writes about performance has a confession coming. I've spent the past week telling people to shrink their images and audit their JavaScript — so this afternoon I pointed the same tooling at my own site. The content pages were fine. The tool pages were not.
Short version: my blog and legal pages ship about 190 KB of gzipped JavaScript. My tool pages ship about 571 KB — roughly three times as much. And the single largest reason is that the JSON formatter downloads a PDF engine.
What I measured
I ran a production build (next build then next start), fetched a handful of representative pages with curl, parsed the _next/static/*.js references out of each page's HTML, and summed the gzipped size of every chunk that page actually loads. Here are the real numbers:
| Page | HTML raw | HTML gzip | JS files | JS raw | JS gzip |
|---|---|---|---|---|---|
/ (home) | 151.3 KB | 20.2 KB | 9 | 638.6 KB | 190.0 KB |
/blog/…-jpg-mistake | 89.2 KB | 15.4 KB | 9 | 638.6 KB | 190.0 KB |
/privacy-policy | 78.2 KB | 12.7 KB | 9 | 638.6 KB | 190.0 KB |
/convert/kg-to-lbs | 78.3 KB | 11.2 KB | 10 | 656.0 KB | 193.8 KB |
/zh | 81.3 KB | 11.2 KB | 9 | 638.6 KB | 190.0 KB |
/tools/json-formatter | 77.0 KB | 12.0 KB | 12 | 1825.7 KB | 570.9 KB |
/tools/image-compressor | 80.7 KB | 12.6 KB | 12 | 1825.7 KB | 570.9 KB |
Two things jump out. First, the HTML is *not* the problem: the homepage is 151 KB of markup raw but 20 KB gzipped — an 87% squeeze. Every page compresses down to 11–20 KB of HTML. Second, the tool pages all load the *exact same* 1,825.7 KB of JavaScript, to the byte, whether you're on the JSON formatter or the image compressor. Identical byte counts across unrelated tools is a smell.
The 726 KB chunk that has nothing to do with the page
Sorting the chunks by size, the top three are:
- 725.9 KB — contains
pdf-lib,PDFDocument,fontkit,startxref,pako - 461.0 KB — also
pdf-lib - 221.0 KB —
react-dom
That first chunk is a PDF-generation engine with an embedded font toolkit. It loads on the tool pages and *not* on the content pages. Which means: open the JSON Formatter and your browser downloads a PDF library. Open a password generator, same thing.
Why it happens
My tool routes share one barrel file: src/app/tools/slug]/tool-components.tsx maps every tool slug to a component with a static import. A static import means "bundle this no matter what," so the tool route's client bundle ends up containing *all* the tools — image tools, text tools, and the [PDF Merge & Split tool with its pdf-lib dependency. One page pulls them all in.
This is the classic barrel-import tax, and Server Components hide it beautifully: my HTML got tiny because rendering moved to the server, but one imperative import in a shared file quietly undid the win on the client.
What I tried (and why it's still not fixed)
The obvious fix is to load each tool's code only when that tool is opened. I tried three routes, and none of them cleanly worked under the bundler this site builds with:
next/dynamicinside the server component: the bundler folded it back into a static import, so nothing split (still 16 chunks).- A client-side
ToolLoaderwith a dynamic map: this finally split the tools apart (16 → 84 chunks), but a shared ~465 KBpdf-libchunk still got preloaded on every tool page — including the JSON formatter. - Lazy
import("pdf-lib")inside the two PDF tools: the bundler hoisted it right back into the eager tool chunk.
I also owe you a precision correction to my own framing above: pdf-lib was preloaded, not executed — the JSON formatter downloaded it but never ran a line of PDF code. That is still pure waste (bandwidth), just not a parse-time cost, and I would rather state it accurately than let the bigger number stand on its own.
So the static map of every tool inside a single [slug] route keeps pdf-lib in the eager chunk no matter how I lazy-import it. A real fix means either switching the build's bundler or giving each tool its own route — and I want to test that properly before shipping. I am deliberately leaving this as a documented, honest TODO instead of publishing numbers I have not verified. When I do land it, the before/after figures will go in this exact table.
How to reproduce
next build && next start, then for each URL run curl -s "$URL", grep the /_next/static/….js sources out of the HTML, and sum the gzipped size of those files with gzip -9. Measuring the chunks a page *actually references* is more honest than trusting one "total bundle size" number, because it's exactly what the browser will fetch for that URL.
The uncomfortable lesson: it's far easier to see another site's bloat than your own. If you run a tool or component directory, go count the bytes on two pages that shouldn't share code — identical numbers will tell you exactly where the leak is.