How this site was built
Last updated: August 1, 2026
Why this page exists
The working method
Skills and plugins
The stack
Performance, measured
What I got wrong
More things I got wrong
The screenshots nobody scrolls throughmemory
I put full-page screenshots on the portfolio cards, each 6–14 screens tall. During a card's roughly one second of focus a visitor sees less than one screen of it – yet every machine pays for the whole thing in decoded bitmap: 15 images, 217 MB of memory. The WebP file sizes looked fine, so I never noticed. I cropped them all to 1800 pixels; with images the cost isn't file size, it's decoded memory.
Beautiful, and dropping framesperf
I loaded the site with Awwwards-style animation, and under the showiest piece the performance collapsed. The work section's images animated clip-path and transform at once: Chrome ran it smooth, while WebKit repainted the clipped image subtree on the main thread every frame – I measured 43–80 ms frames mid-scroll. Stuttering, even the most careful motion reads as a cheap trick. I rewrote it as compositor-only opacity + transform at the same tempo: the look stayed, the jank left. I had to learn this one too – an effect is worth exactly what it can do at 60 fps, and I measure in two browsers now.
The promise that lied for one framea11y
Smooth scrolling read the prefers-reduced-motion setting from React state that an effect filled in – so the first render always saw motion enabled, then flipped. Anyone who had turned motion off still got one frame of it. I moved the read to useSyncExternalStore: the very first frame now sees the real setting.
The will-change that looked like optimizationperf
I meant will-change as a speed-up and sprinkled it on everything that animates. But will-change doesn't speed anything up – it pays up front: every marked element gets its own compositor layer and holds memory while nothing moves. I stripped it back; now it goes on for the duration of a tween and comes off at the end.
Redis under a one-person siteoverengineering
I wired an external Redis service into the contact form's rate limiter – for a portfolio form that gets a handful of submissions a day. I removed it: an in-process counter does the same job with one dependency and one API key less.
In numbers
409 commits · 89 active days · peak 34
