Skip to content

Forty-one to eighty-eight

Showrunner’s mobile PageSpeed score was 41 (boo!). It is now 88 (yay?). This is a technical note on how that happened, mostly because the most useful lesson from the whole exercise was far from what I expected, and maybe that will help someone who goes Googling looking for a solution.

Showrunner is built on Next.js, React, TypeScript and Tailwind, with Supabase as the backing store. The public pages stream server-rendered content, the admin side is where I actually write posts. I’d noticed that the site’s Performance score wasn’t terrific: 41/100. I know enough not to obsess too much on the specific score, but that it could be drastically improved by focusing on the specific metrics that contribute to the big number.

I had some ideas of where to start, but for this I recruited both Claude Code and OpenAI’s Codex into this process at various points, whether diagnosing issues and suggesting approaches, or carrying out the larger structural changes.

When you break down that score of 44, the starting position wasn’t a disaster. CLS was already zero. Images were AVIF/WebP. next/image was everywhere it was supposed to be — except, crucially, inside markdown content, where the rendered HTML was being injected via dangerouslySetInnerHTML and bypassing the image pipeline entirely. Claude flagged this as the likely LCP bottleneck. It was right.

Codex proposed a four-part plan. I agreed on the targets, pushed back on the scope (a “homepage-only” markdown renderer, when the cards in question render on /writing, /tag/[slug], /search, and inside the load-more flow), and asked for the changes to be sequenced so each one could be attributed to a number. That last bit turned out to matter more than anything else.

The first four

Deleting app/loading.tsx was a one-line change. The root loading skeleton was wrapping the entire public tree, which meant Lighthouse was measuring the skeleton as the LCP candidate rather than the actual timeline content streaming in behind it. Score climbed into the mid-50s.

Wrapping getAllSiteSettings in React 19’s cache() deduped three Supabase round-trips per request down to one. Small but real.

Rewriting getTimelineItems to use limit + 1 instead of count: "exact" saved Postgres a full row count on every query. The performance win was minor; this was more of a hygiene change.

Then the big one: overriding md.renderer.rules.image so markdown-rendered images emitted /_next/image URLs with srcset, sizes, dimensions, and fetchpriority for the first image on a priority card. (Cards are the React components that render each post in the timeline — one per post type, so a TextPostCard, an AlbumPostCard, a LetterboxdCard, and so on.) The dimensions came from the upload handler reading them out of the buffer at the time of upload — stored in posts.metadata.inline_images, JSONB, no migration required. After this landed: Performance 65, LCP down from 15.2s to 4.5s.

The change that did the work

PageSpeed at 65 was respectable, but it wasn’t good.

Wiring in the bundle analyzer made the next move obvious. A 109 KB chunk on the public bundle was entirely markdown-it and its dependencies — markdown-it-footnote, entities, linkify-it, punycode, mdurl. The homepage was shipping a full markdown parser to the client, even though the server already had all the markdown and was perfectly capable of rendering it. The cards were importing renderMarkdown directly, the dynamic-imported LoadMoreButton was prefetching them, and so the parser was riding along with every public page.

The fix was conceptually simple and structurally invasive: render HTML at the query layer, attach it to the post as content_html, and strip the @/lib/markdown import out of every card component. Seven cards lost their markdown imports. RecapPostCard lost its editorial-slicing helper (the slicing now happens server-side, once). VideoPostCard’s embed extraction simplified for the same reason. The RSS route got a { optimizeImages: false } flag so feed readers receive absolute URLs rather than /_next/image paths.

Bundle analyzer confirmed it: markdown-it gone from public bundles, shipping only with the admin editor. Performance jumped from 65 to 88. LCP 4.5s to 2.9s. TBT nearly halved — exactly what you’d expect from pulling a parser off the main thread.

This was the whole phase, really. The rest was setup and cleanup.

The lesson worth writing down

At 88, PageSpeed flagged “613 KiB unused JavaScript” and “99 KiB unused CSS.” I went looking for what was left to cut. There wasn’t much. Total production CSS was 8.7 KB gzipped — Lighthouse’s “99 KiB unused” figure was larger than the entire stylesheet, an artefact of how it counts(?). The top four JS chunks were framework, react-dom-client, the Next.js client runtime, and main. All untrimmable. We were sitting on the framework floor.

The only real lever left was Google Analytics. afterInteractive runs gtag.js as soon as React hydration finishes; lazyOnload waits for window.onload. Deferring further was the first thing that occurred to me. I changed two strategy attributes.

Performance went from 88 to 67. TBT went from 250ms to 870ms. Oh dear.

The theory had been plausible: get GA out of the hydration window, free up the main thread, save some TBT. The measurement said the opposite. On Lighthouse’s throttled mobile, lazyOnload shifted GA’s parse and eval to land inside the TBT measurement window, where it counted as blocking. afterInteractive had let it ride alongside hydration, which Lighthouse already budgets for. The counterintuitive answer was the right one: defer less, not more.

Reverted, documented, moved on.

What I’d take from this

Sequence changes and measure each one. Without the per-change attribution, I’d never have known the server-side markdown render was worth twenty-three points on its own — and worth the structural disruption it caused.

The framework floor is real. Once React and Next.js dominate the bundle, Lighthouse’s “unused JS” metric becomes misleading. It’s coverage noise rather than a lever.

“Homepage-only” is almost always the wrong scope boundary when components are shared. The pre-rendering refactor worked because it was the same code path everywhere.

And measurement beats intuition, every time. The lazyOnload reversal cost twenty-one points on a change that any reasonable person would have shipped without checking. That one’s now an invariant in CLAUDE.md, recorded specifically so I don’t try it again, and nor does Claude.

Farming 101

no01.substack.com

Opens in full prepper register (Strait of Hormuz, fertiliser shocks, food prices coming for you in 6 to 18 months) and I nearly stopped reading. But once the doom clears, it turns into a genuinely useful primer on growing at home from seed: what to start indoors, what goes straight in the ground in April, why soil matters more than anything else, and the unromantic economics of a £3 tomato packet. I grow a fair bit from seed myself—herbs, tomatoes, courgettes, strawberries, even purple cauliflower this year—and there’s something to the argument that you get to enjoy more of nature’s lifecycle this way, rather than the garden-centre-to-windowsill-to-compost-bin shortcut. Worth it if you’re thinking about planting anything this year.

NMS Ceefax

nmsceefax.co.uk

A fully working Ceefax service, kept up to date with real news, weather, sport and TV listings, viewable through an interactive on-screen remote that behaves exactly as you remember. Nathan Dane has been building this since 2015; what started as a home-broadcast hobby in his attic grew into custom PHP scrapers, hand-soldered VBIT-Pi inserter boards, and eventually a YouTube stream of Pages From Ceefax decoded through period-correct hardware. Page 302 for the football.

WikiCity

wikicity.app

Reid Lewis has built a 3D city out of the 100,000 most-viewed Wikipedia articles from the past year: every building an article, every floor a pageview count. It's inspired by Samuel Rizzo's GitCity, which does the same trick for repositories. You can click buildings to open the article, or—naturally—fly around in a little plane and blow them up.

Pompei: Below the Clouds

Pompei: Below the Clouds

Gianfranco Rosi·2025·★★★★

No narration, no score to speak of, no panning over the bay of Naples at golden hour. Instead, fixed cameras pointed at archaeologists brushing ash off a shinbone, emergency services staff answering calls from scared residents, a tutor helping children learn a variety of subjects, Syrian workers bringing in thousands of tonnes of Ukrainian grain. Vesuvius looms in almost every frame, usually grey, often half-swallowed by cloud. Everyone on screen is, in one way or another, in its shadow: extracting from it, measuring it, living despite it.

film·mubi

There’s a Good Reason You Can’t Concentrate

www.nytimes.com

Cal Newport argues that just as diet and exercise became cultural common sense in a generation, we need a similar shift around “mental fitness” by treating sustained attention as something to train rather than concede. The framing is useful, though it covers ground Nicholas Carr mapped fifteen years ago in “The Shallows” (the net as “an interruption system, a machine geared for dividing attention”). I am unsure whether a fitness-style cultural shift can actually take hold when the incentives of every device in your pocket run the other way.

What's the point of hardbacks?

tomrowley.substack.com

Tom Rowley asks publishers, agents and the boss of the Booker why fiction still debuts in hardback when readers clearly prefer paperbacks. The answer is margin; the first edition is a “glorified marketing tool” for the paperback a year later. Indies are already breaking the pattern: Fitzcarraldo has always done paperback-first, and Faber recently published Eliza Clark’s “She’s Always Hungry” in both formats simultaneously. Fine for non-fiction and cookbooks. For a novel you want to shove in a bag, less so.

If I Had Legs I'd Kick You

If I Had Legs I'd Kick You

Mary Bronstein·2025·★★★★

Rose Byrne is almost the sole focus. There are several important voices that we barely see. A formal choice that doubles as the argument. What it generates is noise. Overwhelm, guilt, the low hum of panic even when nothing’s on fire; and here, quite a lot is.

film·bfiplayer
Spirited Away

Spirited Away

Hayao Miyazaki·2001·★★★★½

My son’s first Miyazaki, and—with apologies to everyone who’s been telling me for twenty-odd years—mine too. Enjoyed the unhurriedness: no villain, no ticking clock, no third-act lesson, just Chihiro earning her courage by the minute. Watching it with an eight-year-old turned out to be the antidote to arriving at canon late, burdened by other people’s readings. The boy had no readings, just a quiet “wow” at the train across the water. Now we have the rest of the shelf to work through.

film·netflixfabian
It's The Long Goodbye

It's The Long Goodbye

The Twilight Sad·2026·Rock Action Records

The Twilight Sad have spent twenty years working a seam between Scottish indie, goth, and shoegaze—three sounds that have drifted in and out of fashion around them. It’s The Long Goodbye is their first record in seven years, and it arrives at a moment when shoegaze has gone from a minor concern to something close to a default setting for young guitar bands. Good timing.

James Graham wrote the album while watching his mother live with early-onset dementia. I didn’t know that on first listen—I learned it later, and the lyrics rearranged themselves accordingly on the third or fourth pass. The title stops being a phrase and starts being a description.

Likely to find them their biggest audience yet, and deservedly so.

album
Palm Springs

Palm Springs

Max Barbakow·2020·★★★★· Rewatched

The time loop genre deserves more entries, not fewer, and Palm Springs is the argument for why. Strip away the gimmick and you’re left with the oldest question there is: what would you actually do if consequences were suspended? Groundhog Day answered moral growth. Palm Springs answers commitment, and the particular flavour of nihilism that sets in once you’ve already tried everything else, and it turns out that’s the richer question. Samberg is restrained here, playing a man who’s been funny for so long he’s forgotten why. Milioti is the whole film.

film·amazonprime
Honey, I Shrunk the Kids

Honey, I Shrunk the Kids

Joe Johnston·1989·★★½· Rewatched

Continuing to show my son the high-concept family comedies of my youth—a genre Hollywood was unusually good at in the late eighties and early nineties, when a one-line premise could carry a whole film. This one was well-received. Some of the practical effects still hold up. The digital work does not—1989 was a year or two too early, and the seams show in every composite. Memories of watching it at the cinema with my grandmother aren’t quite enough to lift it any higher.

film·disneyplusfabian

Fabian's Arena

Fabian’s Quest handled the classroom side—times tables, spelling, the nightly homework loop. This one exists because the kid is obsessed with football in a way that borders on clinical.

He’s had us both up before seven every day this past week, out at the park for over an hour before breakfast. This isn’t unusual. Other children on his team play their Saturday match and that’s football done for the week. Fabian would play for eight hours a day if you let him, and he’d be annoyed when you stopped. The app was his idea—he wanted something that would make our training sessions better, not just longer.

The problem was familiar: we’d show up with a ball, a bag of cones and a vague intention to “work on shooting,” which meant twenty minutes of him firing shots at me followed by forty minutes of whatever felt easy. Enjoyable, but aimless. He knew it was aimless. He told me.

Fabian’s Arena gives those sessions structure while keeping everything fun—which is the whole point when you’re eight. It generates 60-minute blocks (warm-up, two skill rounds, a 1v1 match, cool-down) with drills drawn from a library of 30 across six categories: dribbling, passing, shooting, first touch, speed and agility, defending. Each drill comes with coaching tips, a suggested duration, and a linked YouTube tutorial.[1]

There’s also a spin wheel for when you just want one quick drill picked at random, which is most Tuesday evenings.

The visual language is dark pitch-green with amber and gold accents, glassmorphic cards, and the same Fredoka headings used in Fabian’s Quest. It runs as a PWA on my phone, works offline at the park. Progress data persists locally via Zustand and syncs to Supabase when there’s a connection, which means nothing is lost between the front door and the goalposts.

Some details worth noting:

  • The session generator uses weighted random selection, boosting underrepresented skill categories and biasing toward a chosen focus skill—so sessions feel varied without being chaotic
  • A badge system with 17 achievements across milestones, streaks, and per-category mastery, surfaced through a full-screen celebration overlay that an eight-year-old finds deeply satisfying
  • The spin wheel is an SVG with Framer Motion rotation—ten random drills on coloured segments, four to six full spins before landing
  • Drills show a suggested duration and a manual “Done” button. I’d toyed with an in-app timer, but phone screens lock during actual training, which kills any running countdown
  • The 1v1 match block has no drills—it’s twenty minutes of free-play football, Dad vs Fabian, rendered as its own card in the session flow

Built with Next.js, TypeScript, Tailwind and Framer Motion. A companion to Fabian’s Quest rather than a sequel.


  1. I watched hours of kids’ football tutorial videos. The quality YouTube search returns is remarkably variable—even with precise coach-speak in the search terms, I’d get anything from a Premier League academy production to a man kicking a beachball around a garden with a toddler. ↩︎

Under the Silver Lake

Under the Silver Lake

David Robert Mitchell·2018·★★★★

I’m a complete mark for films where some aimless nobody pulls at a thread and gradually uncovers a conspiracy several orders of magnitude bigger than they bargained for. Under the Silver Lake knows this about people like me and exploits it ruthlessly.

Andrew Garfield is perfectly cast as a repellent protagonist—no job, no aspirations, no redeeming qualities, and (the film is at pains to remind us) he literally stinks. The whole thing drips with cynicism about Hollywood and that part of LA, landing somewhere between David Lynch and a paranoid Reddit deep-dive. It’s the sort of film you’ll either love or find completely insufferable.

film·mubi
Hurts Like Hell

Hurts Like Hell

Charlotte Cornfield·2026·Merge Records

Cornfield’s first record since becoming a mother in 2023, and it sounds like the perspective shift has unlocked something. The pedal steel (courtesy of Adam Brisbin) threads through the album beautifully—country-tinged without ever tipping into full country, giving even the more vulnerable moments a warmth and sway. It’s her most collaborative album to date, and the guest list reflects good taste and good company: Buck Meek, Feist, Christian Lee Hutson. There’s something worth noting in that openness—becoming a parent seems to have made her more willing to let other voices in, both literally and in how she writes. The themes of renewal and perseverance through awkwardness land without ever feeling heavy-handed. Closer “Bloody and Alive” addresses motherhood most directly, spare and unguarded, and it earns the weight it carries. Highly recommended.

album

The sketch-to-feature problem is real (you can feel the last twenty minutes stretching), but the hit rate on individual gags is high enough to carry it. Impressively stupid, in the most complimentary sense.

film·netflix

Fabian's Quest

My son is in Year 3 and needs to practise times tables, spelling, and a handful of other subjects every night. The available apps were either too broad, too patronising, or too keen on subscription revenue. I built him something instead.

Fabian’s Quest is a learning app dressed up as an RPG. There’s a Daily Quest—37 questions across seven rounds covering maths, scales, spelling, grammar, science, history, and geography—designed to take about ten minutes. Short enough that it happens every night regardless of energy levels. You can also take a deeper dive into any of the subjects on their own. Each correct answer earns XP, streaks build, levels unlock. The kind of feedback loop that works on a child who treats everything as a competition.

It runs on an iPad, built with React and Vite, deployed to Cloudflare Pages. No backend—player data lives in localStorage, which is fine when your entire user base shares a device and a surname. The visual language is dark navy with gold accents, chunky rounded type, and the sort of progress bars that make eight-year-olds feel like they’re levelling up in a proper game. (Claude helped with the styling—it’s not my thing at all.)

Some details worth noting:

  • Procedurally generated measurement questions with inline SVG number lines—no static image assets, no question bank to exhaust
  • A spelling hint system that auto-detects patterns (double letters, silent letters, tricky vowel pairs) and generates contextual clues rather than just revealing the answer
  • Programmatic sound effects via the Web Audio API and text-to-speech via the Web Speech API—zero external media files
  • The entire application lives in a single 3,200-line React component, which I mostly think is admirably focused but occasionally see as a future problem
  • I’ve built a workflow with Claude Code to automatically update it each week when the school sends home new material

The name was Fabian’s idea. He wanted a quest.

The Doki Doki Panic sequels

A beautifully committed April Fools bit: what if Doki Doki Panic got the same sequel treatment Mario gave it, but in reverse? So there’s a Doki Doki Panic 3, a Doki Doki Panic 64, and so on—each one a reskinned Mario game, presented with a straight face as a genuine series retrospective. The mock-ups are genuinely impressive.