Putting the boat on a diet

Projects
Performance, Webpack, Elixir
July 12, 2026

Popsicle Boat’s whole pitch to bloggers is: link your readers here, and we’ll be respectful of them — no ads, no tracking, no scripts riding along on your site. It occurred to me to check what happens on my site. A reader clicks “Discuss this post →” — how many bytes am I asking them to pay for the privilege?

I measured a logged-out thread page. 843 KB. For context, this blog has a membership patch pending at the 512KB Club, a directory of sites whose entire page weight fits in half a megabyte. The boat I was proudly linking from my application-in-waiting weighed almost double the limit.

The ledger #

Bytes
JavaScript325 KB
Images276 KB
CSS229 KB
HTML13 KB

Cut one: the call system nobody logged-out can use #

A third of the JavaScript was the WebRTC video-calling machinery — signaling, peer connections, reconnect logic, the call dock. All of it, shipped to readers who cannot place a call, because calling requires an account.

The fix was a webpack dynamic import: the entire call system moved into a chunk that only loads when the page carries a signed-in signal. Three seams cross the async boundary — a LiveView hook that might mount before the chunk arrives (it queues), the dock’s call integration (it already had no-op defaults, bless the past), and the morphdom callback from the first panel bug (the attribute preservation stays eager; the forensics can be late). The two-browser call smoke test passed first try, which after this series of posts felt like a personal gift.

app.js: 325 KB → 213 KB. The call chunk: 112 KB, members only.

Cut two: the part I didn’t see coming #

The header logo — an ankh rendered about 40 pixels tall — was being served as a 923×1352 PNG. Twice (light and dark variants). With the icon file, that’s 244 KB of images for a glyph smaller than a thumbnail. The JavaScript took a week of careful surgery across an async boundary. The images took one ImageMagick command: 244 KB → 9 KB.

The lesson stung pleasantly: I’d been performance-tuning the part of the page I found interesting. The dumb weight was three files nobody had looked at since they were exported.

Found along the way #

Two real bugs were hiding in the static pipeline. The digested favicon URL in every page head had been returning Not Found for months — Plug.Static’s only whitelist matches favicon.ico exactly, and the cache-busted favicon-<hash>.ico never qualified; browsers silently fall back, so nobody ever saw it fail. And gzip serving was off entirely, even though the build had been dutifully producing .gz files nobody served.

The ledger, after #

StoredOn the wire
JavaScript213 KB65 KB
CSS217 KB39 KB
Images24 KB13 KB
HTML13 KB13 KB
Total468 KB~130 KB

A full LiveView social app — with a video-calling system still aboard, invisible until you sign in — now costs a reader less transfer than most sites spend on web fonts.

And 468 is less than 512. My membership patch is still waiting in the club’s queue — but at least the boat this blog links to would now qualify alongside it.

Discuss this post aboard Popsicle Boat →