Utlty has 75 tools today, and most of them do heavy work: encoding JPEGs with MozJPEG, optimising PNGs, decoding iPhone HEIC photos, rewriting PDFs. The code that does that work is big. The PDF encryption library alone is 443 KB gzipped.
Yet across all 75 tool pages, the JavaScript that loads before you touch anything is 20.2 KB gzipped at the median and 31.5 KB at most. This post covers how, and the checks that keep it that way.
The budgets
Every page has two hard limits:
- 40 KB of gzipped JavaScript before interaction: every script the page loads and everything they import statically, plus inline scripts.
- 15 KB of gzipped CSS, counted the same way.
These aren’t guidelines. After every build, scripts/check-dist.ts parses every HTML file, follows each script’s static imports through the bundle, gzips what it finds and fails the build if a page is over. The same script checks that no page loads anything from another origin, and runs the SEO checks: title and description length, one <h1>, canonical URL, sitemap entry, valid structured data.
A check that runs on every build doesn’t depend on anyone remembering the rule.
Where the bytes are today
From the current build:
| Page | JavaScript (gzip) | CSS (gzip) |
|---|---|---|
| Pages with no tool (hubs, blog, 404) | 4.0 KB | 12.0 KB |
| Smallest tool page (word counter) | 15.6 KB | 12.0 KB |
| Median tool page | 20.2 KB | 12.0 KB |
| Largest (signature resizer) | 31.5 KB | 12.0 KB |
| Budget | 40 KB | 15 KB |
Static HTML first
The site is built with Astro as plain static HTML. Every tool is a real page with its instructions, tables and FAQ in the markup, so it renders and reads fine before any script runs. Navigation, including the sidebar with every tool, is HTML and CSS: <details> for the categories and the popover attribute for the mobile drawer. No JavaScript.
The interactive part of each page (the drop zone, the controls, the result) is a Preact island. Preact’s core is 4.4 KB gzipped and its hooks another 1.2 KB, so the framework is a small share of every budget. The rest is the tool’s own UI code.
CSS is Tailwind, which only emits the classes pages actually use, inlined into each page so first paint needs no extra request. Fonts are the system’s own, so nothing downloads there either.
Heavy code waits for intent
The codecs are the real weight. Measured from the build, gzipped:
| Code | What it does | Size |
|---|---|---|
| MozJPEG | JPEG encoding | 59 KB |
| OxiPNG | PNG optimisation | 76–101 KB |
| libwebp | WebP encoding | 114–126 KB |
| qpdf | PDF passwords | 443 KB |
| libheif | iPhone HEIC decoding | 469 KB |
None of it loads with the page. Each tool’s work happens in a Web Worker, and the worker imports its codec with a dynamic import() the first time it’s needed. The page doesn’t wait for a file to arrive, either: the drop zone starts the download at the first sign of intent, when you hover over it, focus it or drag a file across the window. By the time you’ve picked a file, the encoder is usually there.
The worker also keeps the main thread free. Encoding a 12-megapixel photo takes a while; in a worker it can’t freeze scrolling or typing.
Cross-origin isolation, and its rule
Some codecs run faster with threads, and WebAssembly threads need SharedArrayBuffer. Browsers only allow that on cross-origin isolated pages, so every response carries:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
The cost is that an isolated page can’t embed anything from another origin unless that origin opts in. We took that as a rule rather than a problem: no third-party scripts, fonts, styles or analytics on any page. The page-view counter is our own endpoint, on our own domain. A whole class of slowdowns, tag managers and font swaps and chat widgets, can’t happen.
Lighthouse 100 as a CI gate
CI runs Lighthouse on 11 representative pages (the homepage, a category hub, image, PDF, developer, calculator, health and SEO tools, and the privacy page), each on mobile and desktop. Every category, performance, accessibility, best practices and SEO, must score 100 or the build fails.
Accessibility, best practices and SEO scores are deterministic. Performance is measured, so a busy CI machine can occasionally cost a point. A page that misses on performance alone is audited again, up to three times. A real regression misses every time and still fails.
Two rules in the codebase exist because of this gate:
- Nothing may move after first paint. Islands are rendered to HTML at build time with their initial values, and nothing is filled in by an effect after the page loads. Layout shift is part of the performance score.
- No console errors, ever. Best practices penalises them, so a stray error from a codec counts as a bug.
What was hard
The network, not the code. On the live site, mobile performance first came in at 98–99, not 100. Lighthouse’s mobile test simulates a slow 4G connection, and on a fresh connection DNS, TCP and TLS alone took about 0.6 seconds of simulated time. On a page with little to load, that sets the score. A later re-run reached 100, but it was a reminder that the budget covers our bytes, not the network’s round trips.
A script we didn’t write. Our CDN had an analytics auto-injection feature left on from an earlier site on the same domain. It added a third-party script to every page. Our Content Security Policy blocked it, correctly, which logged an error and cost best-practices points on the live site but never in CI. Turning the feature off fixed it. check-dist.ts catches anything that ends up in the build; the CSP catches anything added after it.
HTML is the heaviest part. With the full sidebar and inlined CSS, a tool page’s HTML is about 140 KB raw but 26 KB gzipped. The repeated markup compresses well, and the sidebar needs no script. Moving navigation to client-side JavaScript would shrink the HTML and grow the budget we care about more.
The takeaway
Most of this is ordinary: static HTML, small islands, code splitting, workers. What keeps it from eroding is that every rule is a check that fails the build, not a note in a style guide. A new tool can’t go over budget, load a third-party font or ship a page without a title, because the build won’t let it.