DEV Community

MinSoo Kim
MinSoo Kim

Posted on

16 requests, 2 of them third-party, 0 ad requests (what a static calculator site actually loads)

I wanted a calculator site that behaves like a document, not like a web app.

Type three numbers. Get the number of paint cans to buy. No account, no dashboard, no round trip to a server, no analytics watching you type. The page, the arithmetic, and the answer.

That one constraint decided almost every technical choice below, and it is also why the numbers are small enough to print in a table. Here is a cold load of wallmath.com, measured in a headless Chromium on 2026-09-10 against the live build:

What Home page Guide page
Total network requests 16 12
Requests to my own domain 14 10
Third-party hosts contacted 1 1
Requests to that host 2 2
Ad requests 0 0
Ad iframes 0 0
External stylesheets 0 0
HTML document 93,838 bytes raw, 26,621 gzipped 97,078 bytes raw

The one third-party host is pagead2.googlesyndication.com. It serves two script files and then stops, which is the part of this post I find most worth writing down.

Fonts: three files, subset, preloaded

Two families: Fraunces for headlines, Source Serif 4 for body text. Loading them from a font CDN would be one line of HTML. Instead they are three woff2 files on my own origin.

File Bytes
fraunces-700-800-latin.woff2 36,620
sourceserif4-400-600-latin.woff2 50,824
sourceserif4-italic-400-latin.woff2 20,092

Three decisions kept that to 107,536 bytes total.

Variable fonts collapse the weight axis. Fraunces ships variable, so one file covers 700 and 800. Same for 400 and 600 in the body face. Six static files became two, and the two together are 87,444 bytes.

Latin subset only. The copy is English and contains no Cyrillic or Greek, so those ranges are cut. It is the largest single saving available and it costs nothing, because those glyphs would never render.

Each file sits in a <link rel="preload" as="font" type="font/woff2" crossorigin> in the head and @font-face uses font-display: swap, so the fetch starts in the same round trip as the document instead of waiting for CSS to parse.

The honest cost of self-hosting: I lose the shared-cache argument for CDN fonts. That argument is mostly gone anyway, because browsers partition the HTTP cache by top-level site, so a font your visitor already downloaded elsewhere does not help you.

CSS and JS live inside the document

There are no external .css or .js files at all. The build inlines 27,933 bytes of CSS and 45,164 bytes of JS into every page, spread over six <script> blocks, the largest of which is 24,877 bytes.

What it buys: first paint needs exactly one round trip. No stylesheet to discover, fetch and parse before anything renders. On guide pages, which are where search traffic lands, that is the difference between text appearing immediately and text appearing after a second request completes.

What it costs: no cross-page caching. Someone who opens three guides downloads the same CSS three times. Gzip softens it, since the whole document compresses from 93,838 bytes to 26,621, but it is a real cost and it grows with the site.

The number that decided it: the inlined document still compresses to under 30 KB. If the CSS doubled I would move it to a file and accept the round trip. I am not inlining on principle, I am inlining while the arithmetic says to.

The JS is larger than the CSS because it holds the calculation engine, which runs entirely in the browser. Nothing about a reader's room dimensions leaves the page. There is no endpoint to send it to.

Diagrams are SVG, illustrations are AVIF

The home page carries 15 inline <svg> elements: wall elevations, plan views, the can and box pictograms, and the room drawing whose walls change colour when you click a swatch. They come out of the same build that renders the HTML, from the same numbers the calculator uses, so a dimension change updates the drawing and the result together.

Inline SVG costs no extra requests and inherits the page's CSS custom properties for colour. The interactive part is DOM manipulation on those elements. No canvas, no chart library, no animation framework.

The illustrations are <picture> elements with an AVIF source and a WebP fallback, three widths each. What the browser actually fetched on a 1440px desktop view was the 480px AVIF files, because that is the size the cards render at:

Asset Bytes
calc-paint-480.avif (what actually loaded) 11,221
calc-paint-480.webp (fallback tier) 18,514
calc-paint-1200.avif 42,493
calc-paint-1200.webp 78,512

I list the fallback numbers because it is easy to quote your largest file and call it your image weight. The file that ships to most readers here is the 11 KB one.

The ad script that does not ask for an ad

The site is being prepared for AdSense. I added the AdSense loader to the head as part of that preparation and deliberately paused ad requests until the ad placements and the consent flow are ready:

<meta name="google-adsense-account" content="ca-pub-XXXXXXXXXXXXXXXX">
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-XXXXXXXXXXXXXXXX" crossorigin="anonymous"></script>
<script>(adsbygoogle=window.adsbygoogle||[]).pauseAdRequests=1;</script>
Enter fullscreen mode Exit fullscreen mode

pauseAdRequests = 1 is documented by Google as a way to stop ad requests from being made, and setting it back to 0 resumes them. The reason I want it is that the Auto ads setting lives in the account, not in my repository. It can be toggled from a browser I am not looking at, and I would rather the page refuse than trust that nobody does. Ads on this site are going in two places I chose, before the first subheading of a guide and at the end of it, and nowhere else.

What the browser actually does with that markup, from my own network log on the live site: it fetches adsbygoogle.js, that file fetches show_ads_impl_fy2021.js, and nothing further. No /pagead/ads call, no doubleclick host, no gen_204 beacon, no ad iframe. Two requests to one host and zero ad requests, on both the home page and a guide page.

One detail if you try this: the loader always inserts a hidden <ins class="adsbygoogle adsbygoogle-noablate"> into the body, sized 0 by 0 and display-none. It is created by the script, not by my markup, and it issues no request. My build-time check counts ad units in the HTML I generate, so a runtime element does not trip it.

Keeping it from regressing

None of this holds unless something checks it on every build.

A build verifier renders all 17 pages and asserts 98 invariants: no <link> or <script> points at a host outside the site, every image has alt text, every internal link resolves, and the ad unit count on calculators, the home page and the trust pages is zero. The last run walked 470 links across 17 pages with no errors, and the release it validated is recorded as digest 63ee4554 in build-info.json.

A browser smoke suite loads the built site in headless Chromium and records every request, 321 assertions in total, including which third-party hosts are contacted, that no ad request is made, that the calculators produce the expected numbers from typed input, and that no page overflows horizontally at 390px.

A static gate on the ad markup keeps ad units on guide routes only. If an <ins class="adsbygoogle"> appears anywhere else in the generated HTML, or the publisher ID in the markup does not match config, the build fails. Reserved ad space measured between 0 and 0.0103 cumulative layout shift, against 0.006 for the home page that has no ad slots at all.

Releases are signed before they ship

Every release computes a digest over the exact input file set. Approving one means running a signing script with a private key kept outside the repository, which writes a signature for that single digest. A release gate then refuses to publish if what is about to ship has a different digest, or if the signature does not verify.

So the claims in this post are attached to a reviewed input set rather than to a branch name. When the ad markup changed, the gate additionally refused to resume ad requests until a certified consent management platform was recorded in config, because that one is a compliance requirement and not a preference.

What I have not measured

No field data. Every number here comes from the live production build over a normal connection and from a headless browser. There is no analytics on the site, which is the point, and also means I cannot tell you what a slow phone on a bad network sees.

No comparison against a file-based build of the same content. The reasoning about round trips and gzip sizes is arithmetic, not an experiment.

And the ad picture describes a site whose ads are off at three levels. When the units go live behind a consent flow, the third-party rows in that first table change, and the honest version of this post will need a new table.

The point

None of this is a rule. Inlining CSS is wrong for a site with fifty templates and a returning audience. Self-hosting fonts is wrong if your team ships design changes weekly and needs the CDN tooling. Pausing ad requests is pointless once ads are live and consented.

What travels is the habit underneath: measure what each decision costs on your own site, and keep the architecture no larger than the product needs. For three calculators that take three numbers and return a shopping list, that came out as local computation, a low request count, and a build I can reproduce and sign.

If you have made the opposite call on any of these, what number decided it for you? I would rather hear the threshold than the principle.

Sources

Top comments (0)