---
title: "aadhar.sh/garage/htmx: Break-even at three submits"
description: "htmx 4.0 costs this site 11.46 KiB and saves 3,968 bytes per form submit, measured on the one page built for it. That page is submitted once."
path: "/garage/htmx"
section: "garage"
kind: "content"
updated: "2026-08-28"
source: "https://aadhar.sh/garage/htmx"
---

> Site index: https://aadhar.sh/llms.txt
> Section index: https://aadhar.sh/garage/llms.txt
> This is the Markdown twin of a page on aadhar.sh. The HTML at the source
> URL below is the original, and is hand-written and unminified on purpose.

# Break-even at three submits

[htmx](https://github.com/bigskysoftware/htmx) shipped [4.0.0](https://four.htmx.org/announcements/2026-08-28-htmx-4.0.0-is-released) on the day this page was written, and it is a real improvement on 2.x: 36,716 bytes [minified][minification] against 50,917, with the history cache dropped and `XMLHttpRequest` traded for `fetch`. The size is the easy question. The one that decides adoption is how much of your existing JavaScript the library can delete, and that turns on whether your code is already shaped like a request and a swap. This site's mostly isn't. So the measurement went to the one page where htmx should have won, and it lost by a factor of three.

## The library is priced per visitor

A [hypermedia][hypermedia] library moves rendering to the server and sends HTML fragments back. That trade is good when a page holds many interactions, because the download amortizes across all of them. It is bad when a page holds one, because the visitor pays the whole library to save one response.

So the honest unit is bytes per visitor, and the number you need is the count of swaps a single visitor performs. Everything below is that arithmetic, run on real bytes.

## The page htmx was built for

This site has 54 documents and 17 client modules, and almost none of it fetches HTML. `quiz.js` makes zero network requests. So do `hoist.js`, `infotip.js`, `notepad.js` and `nav-tray.js`. The modules that do fetch read JSON and paint structured interfaces from it, which htmx has no opinion about.

One surface is different. `/coffee` is a booking form: two plain `method="POST"` forms, server-rendered responses, 874 bytes of inline script for the slot picker. Submitting it reloads the document. That is the shape htmx was designed for, so that is where the experiment went.

## What was built

htmx 4.0.0 vendored as a client asset, loaded on the booking page alone, outside the shared shell, so the confirmation and decline pages pay nothing. The form keeps its `method` and `action` as the no-JavaScript fallback and gains three attributes:

```
<form class="book" id="bookform" method="POST" action="/coffee/book" novalidate
      hx-post="/coffee/book" hx-swap="outerHTML" hx-disabled-elt="#submit">
```

The [Worker][cloudflare-workers] grew a branch that answers the body alone when htmx asks for it, and the full document otherwise, which is what a crawler and a visitor with JavaScript off still receive:

```
function bookingReply(req, env) {
  return req.headers.get("HX-Request")
    ? new Response(successFragment(env), { headers: htmlHeaders() })
    : new Response(successPage(env), { headers: htmlHeaders() });
}
```

The build passed with no changes to it. The minifier left htmx byte-identical, since it arrives minified.

## The diff

Both trees went through `bun run perf:snapshot`, which self-builds through the [wrangler][wrangler] dry run and reports [Brotli][brotli] q11, the quality the edge serves. Base is `3f9f2f9f`. Every number here is deterministic, so an unchanged file produces no row at all.

| category | base | head | delta |
| --- | --- | --- | --- |
| client assets, Brotli | 86.35 KiB | 97.81 KiB | +11.46 KiB, +13.3% |
| `htmx.js` | absent | 11.46 KiB | new |
| Worker bundle, [gzip][gzip] | 239.88 KiB | 239.97 KiB | +0.09 KiB |
| pages, Brotli | 571.62 KiB across 54 files | no change |  |
| dictionary deltas | 110 deltas, 498.41 KiB | no change |  |

The page bytes hold still because `/coffee` is rendered by the Worker rather than staged as a file, so its markup lives in the bundle. The whole cost is one new asset.

## What the swap actually buys

This is htmx doing its job well. The confirmation response stops being a document and becomes a pane:

| POST /coffee/book | raw | gzip | Brotli q11 |
| --- | --- | --- | --- |
| full page, today | 17,001 | 5,101 | 4,187 |
| htmx fragment | 515 | 291 | 219 |
| saved per submit | 16,486 | 4,810 | 3,968 |

95% off the wire on that response. If the argument were only about response size, htmx would win this page outright.

## The arithmetic that closes it

11,735 bytes of library against 3,968 bytes saved per submit is a break-even at **2.96 submits per visitor**. A booking form is submitted at most once, so the third submit never arrives.

Move the sliders. The library is a first-visit cost, because the asset is cached afterwards, and the saving lands on every submit whether or not the visitor is new. Those two facts are the whole model, and they are enough to find the region where htmx wins.

library

saved

Per 1,000 visits, Brotli bytes. Library cost is 11,735 per first visit; saving is 3,968 per submit. The booking rate would have to pass the crossover for htmx to pay for itself at the current repeat rate.

The default positions are a guess about a personal booking page, and they are the part of this page you should distrust most. The site records no funnel for `/coffee`, so the 4% is an estimate. Drag it and see how far it has to go.

## Three reasons the rest of the site is worse

The booking page is htmx's best case here. The surfaces that carry the actual JavaScript are worse, for reasons that have nothing to do with the library's quality.

### Rendering would move onto the scarcest resource

htmx returns HTML fragments, so every swap is Worker CPU. This account runs on the Workers free tier, where CPU under sustained load is what has actually taken surfaces down here: a signature fan-out across 28 probes clamped requests at 10 ms and blacked out [the lens](https://aadhar.sh/lens) for a window in August. Client rendering off a payload already in memory costs the platform nothing.

### The caching model runs the other way

The photo tooltips read `/images/exif.json`, which is 55,674 bytes raw and **2,764 Brotli**, warmed once when the browser goes idle, and it carries the metadata for all 158 photographs. Under htmx that becomes 158 separate fragment requests, one per hover, each one a Worker invocation and a [round trip][round-trip-time]. The site front-loads bytes into assets that go nearly free on the second visit, which is the opposite bet.

### Boosted navigation is slower than what already ships

Every document here names a [speculation ruleset][speculation-rules] at `moderate` eagerness in a `Speculation-Rules` header, so the next page is rendered before the click lands. `hx-boost` is a fetch on click, which is strictly later. The cross-document [View Transition][view-transitions] was removed from this site in July for the same reason. htmx 4 also drops the history cache and refetches on back navigation, where the browser's own [bfcache][bfcache] restores the page with its state in place.

## What the snapshot cannot see

A wire-size diff measures files. It cannot measure round trips, and round trips are where the argument above actually lives. So the diff proves the narrow claim, which is that the library is additive on this site's one hypermedia-shaped page, and the three reasons remain unmeasured.

Two smaller things the experiment left on the table. htmx landed as a plain top-level asset, outside the [content-hashed][content-addressing] `/a/` tier, so it received no [shared-compression dictionary][compression-dictionaries] and the delta count held at 110. Giving it that treatment would make repeat visits nearly free and leave the first visit exactly where it is. And the inline slot picker survived untouched, because an `aria-pressed` toggle and a `toLocaleString` button label are not requests. Lines of JavaScript this deleted: zero.

## What would change the verdict

A surface on this site with several swaps per visitor. That is the whole condition. A multi-step form, a filterable list somebody actually pages through, a comment thread: any of those move the submits-per-visitor number above three and the arithmetic flips on its own.

The finding that would falsify the page is a measurement showing the fragment saving is larger than 3,968 bytes on a real interaction, or a Worker rendering cost near zero under load. The first is easy to imagine on a heavier page than this one. The second is the claim the free-tier CPU ceiling makes hard.

watching · the understanding check is part of the page, not a gate

[minification]: https://en.wikipedia.org/wiki/Minification_(programming) "Stripping spaces, comments and long names out of code so it downloads smaller and still runs the same."
[hypermedia]: https://en.wikipedia.org/wiki/Hypermedia "Hypertext extended with images, sound and video; on the web, pages whose links and forms tell the reader what they can do next."
[cloudflare-workers]: https://developers.cloudflare.com/workers/ "Code that runs on Cloudflare's servers around the world and answers each request directly, with no server of the site's own."
[wrangler]: https://developers.cloudflare.com/workers/wrangler/ "Cloudflare's command-line tool for developing, configuring and deploying Workers."
[brotli]: https://en.wikipedia.org/wiki/Brotli "A compression algorithm from Google built for the web. It ships with a built-in dictionary of common web text, which helps on small files."
[gzip]: https://en.wikipedia.org/wiki/Gzip "The oldest compression format every browser accepts for web pages. It's fast and universal, and its files come out larger than brotli's or zstd's."
[round-trip-time]: https://en.wikipedia.org/wiki/Round-trip_delay "One request travelling to a server plus its reply coming back; the time this takes is the round-trip time."
[speculation-rules]: https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API "A browser API where a page lists links the browser may fetch or fully render ahead of time, so the next page appears instantly on click."
[view-transitions]: https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API "A browser API that animates the change between two views or two pages by snapshotting the old one and morphing into the new."
[bfcache]: https://developer.mozilla.org/en-US/docs/Glossary/bfcache "The back/forward cache: the browser keeps a page frozen in memory when you leave, so Back or Forward restores it instantly with its state intact."
[content-addressing]: https://en.wikipedia.org/wiki/Content-addressable_storage "Naming each piece of data by a hash of its bytes, so the same content always gets the same name and any change gets a new one."
[compression-dictionaries]: https://www.rfc-editor.org/rfc/rfc9842.html "A scheme where the browser keeps an earlier copy of a file and the server sends only a compressed difference against it."

Source: https://aadhar.sh/garage/htmx
