Some Long Overdue Demolition
Two months ago I wrote Some Long Overdue Housekeeping. A post about giving this blog a proper facelift. I untangled my Tailwind setup, modernised Hugo and taught it to drive the compiler natively and generally felt very pleased with the state of things.
So, of course just weeks later I having this burning desire to nuke it all from orbit!
This means no more standalone Tailwind config, no committed stylesheet, no
node_modulesinstatic/and no build toolchain leaking onto the live site. Very cool!
To be clear, the June cleanup wasn’t wrong. The pipeline was genuinely tidy. But, a tidy pipeline is still a pipeline and somewhere between then and now I started asking a more annoying question: why does a personal blog, a pile of static text, need a JavaScript toolchain at all? Why do I need to buy into the Tailwind framework when I knew full well how to write vanilla CSS with my own hands? Every npm install was a little tax that I’d eventually have to pay with interest the next time I wanted to update the site’s dependencies. I felt as if I wasn’t maintaining a blog. I was maintaining the machinery that produces a blog and the machinery had some very strong opinions.
So, I did a hard reset with a very satisfying amount of deleting.
The demolition …
Here’s the bill for the redesign commit:
230 files changed, 3951 insertions(+), 33365 deletions(-)That’s roughly a 10-to-1 ratio of deleted to written. Which, as a software engineer, is my favourite kind of ratio. I happily added the following the to chopping block:
- Tailwind v4 and the typography plugin. Replaced by one hand-written CSS file. The whole thing gzips to under
~10kBwith syntax highlighting included. - The entire Node toolchain. The
package.json, lockfile, Prettier and its plugins. There is nonpm installstep anymore because there is no npm anymore. - Fuse.js and the ⌘K search palette. Nobody was searching this blog. I checked. It was me. I was nobody.
- Two self-hosted webfonts. More on this in a second, because it’s the biggest single win.
- The changelog page. This pulled from the GitHub events API and shipped with an
18,016-line JSON fixture for local dev. Eighteen thousand lines of someone else’s API response, committed to my repo. So, a page barely anybody visited could render in development. I mean, I added it back becase I missed it, but it’s much more efficient now. - 68 of the 79 icon SVGs. A 390-line drawer menu and a taxonomy I’d stopped using.
The layouts went from 2,868 lines of template across 48 files to about 800 lines across 21. Not because I got cleverer, but because most of that code was written for the joy of it. Which, look, is a fine reason to build something. It’s just a bad reason to keep serving it to strangers. And with all that complexity and cruft came what felt like a lot of brittleness.
The fonts had to go too.
This one hurt a little. I like Inter and Rubik. Together they were 449kB of woff2 riding along on every first visit. Which made them, by an enormous margin, the heaviest thing on the site.
So, I dropped them and switched to the system font stack which, to my surprise, actually looked better on the site. The actual typeface was doing very little. Your operating system already ships a perfectly good set. All lovingly hinted by people with far more patience and experience than myself. Best of all it costs zero bytes to use!
By Grabthar's hammer... What a savings.
Hugo, finally home alone.
Thanks to all the trimmed fat, the build process is effectively just hugo. No more extended binary, SCSS, image processing or defining NODE_VERSION in Cloudflare Pages required. Fewer dependencies mean fewer moving parts and that’s always an improvement.
Numbers, or it didn’t happen.
Right after the deploy I proudly measured the new site and announced it was “under 100kB”. Which would have been true if the production site were served by my dev server, uncompressed, with a 40kB livereload script attached. I made the mistake of measuring the network stats on my local development server. Womp, womp.
The real numbers, from the live site with Cloudflare’s brotli doing its thing:
| Before | After | |
|---|---|---|
| Cold home page | ~750kB |
~24kB |
| Webfonts | 449kB |
0kB |
| Stylesheet ( compressed ) | it’s complicated | under ~10kB |
npm install |
required | there is nothing to install |
The single largest asset on the home page is now the favicon. 7.5kB; roughly a third of the total weight. I think you’re allowed to stop optimising when your largest single asset is your favicon.ico. It’s an image of my gorgeous mug. It stays. I’m quite attached to it.
An ambitious first pass.
In retrospect, I wanted to make everything simpler and unintentionally over-compensated in the opposite direction. I stripped out too much and was now staring at a sad little blog with zero personality.
So, the second half of the job was building the personality back and making my selections a bit more deliberate. One thing led to another and I think I’ve produced my finest work yet on a personal blog. It’s small and lightening fast. I’m not sure where else I can optimise without drastically affecting the overall vibe of the design; the law of diminishing returns… returns.
The thing about now having such a light frame is adding to it stops feeling like a burden, or something future Wilhelm will have to pay. For example, here are the biggest things I’ve added and they barely increased the overall “weight” of the design.
Series
A proper taxonomy for multi-part writing. Built because I’ve been wanting to write a Cyberpunk-themed set of tutorials for writing advanced HCL in Go. The series will be about eight posts long and outside of sharing a tag, I had no way to string them together. Series posts wear a stacked-sheet card, a segmented progress meter that is absolutely not a cyberdeck RAM readout and a panel showing every part in the run, including the ones that don’t exist yet.
The unpublished parts of a series ship as glyph noise and decode, left to right, when you point at them. The real titles sit in aria-label the whole time, so a screen reader is never handed the static. The browser tab pulls the same trick on every page load, because a title bar is just a very small marquee nobody was using.
Try moving your mouse pointer over some of the “encrypted” titles on the new dedicated page.
Tired LEDs
The lit chips in the series meters, static leaders in the main page’s hero section and a scattered handful of cells in the activity graph flicker like hardware that has seen some things. Their animation clocks share no common factors, so the pattern never visibly repeats. The same no-RNG randomness as the heading glitch. On page load the whole graph boots like an old panel, dark, then a sputter per cell, then steady stream of blinking boxes. Reminiscent of the old monochrome screens and beige boxes of my youth.
Christ, I’m getting old.
Changelog, resurrected
Yes, the one from the butcher’s block above. However, instead of hitting the associated Github repository directly at build time using a PAT, the new one doesn’t call anyone. Commit data is sourced directly from Cloudflare Pages’ shallow clone it checks out during build time. There’s a simple 20-or-so-line shell script reads git log and Hugo renders the result. HEAD gets a lit node that flickers, because why the hell not?
Image shortcodes
Something I’ve been meaning to add is support for various ways to display images. Each image can now be clicked-to-zoom with attribution in the following forms:
- Masonry.
- A scrollable, horizontal display.
- Last, but not least, the typicla single image display.
There’s actually a sneaky, super-secret page I use to test all my shortcodes and typography stylings where you can see everything for yourself.
Even with all these complex additions, it all adds up to a few kilobytes of CSS and inline script. Which is to say the entire second wave of features weighs an order of magnitude less than one of the webfonts I deleted.
My new favourite fidget toy.
The part I didn’t expect was to glue my eyeballs to the Firefox throttle menu under its network tools panel.
I flipped it to a 2G profile just for shits and giggles - call it a generous 50kb per second - and did the napkin math. The old site’s 750kB works out to between 60 to 90 seconds of staring at a blank viewport. The new site lands in about four seconds on a connection profile most of the industry stopped thinking about a decade ago.
I have reloaded this page on simulated garbage networks far more times than any reasonable person should, and it is satisfying every single time. The page just… appears. Like a website. Remember plain’ole websites?
None of this is novel, I know. The “your blog doesn’t need 2MB of JavaScript” essay has been written a hundred times. Usually on a blog serving 2MB of JavaScript. I’m not even the hundred-and-first person to do this. But, there’s a difference between nodding along to the idea and watching your own site load over a simulated tin-can connection before you can finish blinking.
In closing …
The funny thing about the June post is that it ended with me feeling like the blog was finally sorted. And it was! For the architecture it had. It just turned out the architecture itself was the thing that needed to go.
The site is now boring in the best possible way. It builds with one command, it has no dependencies to rot behind my back and the whole design fits in one stylesheet I can actually hold in my head ( not really ). Fewer moving parts, fewer things to forget, fewer surprises the next time I touch it in six months. Best of all, it looks fucking great!
Set-and-forget, but this time with almost nothing left to forget. … Right?
Comments