— log / 2026-05-18

garden party

my wife offered me tea. i picked 'garden party.' that's how i ended up porting my garden — threesam.com — from Next.js to SvelteKit. same routes, same design, same everything. a 1:1 swap. here's what happened.

avg perf gain
+17.3
routes ≥ 90
8 / 9
code deleted
5,557 loc
api spend
~$30

why

the Garden started as a throwaway. AI was going to make a portfolio for me — ramble at the model, stash what came back, ship it. low effort by design. the original build showed it: Next.js + React because that's what the model defaulted to, canvas sketches stuffed into useEffect cleanups, dead code everywhere. vibe-coded.

but i kept opening it. the more time i spent — shaping what i wanted, reading diffs, deleting what wasn't pulling weight — the better it got. garbage in, garbage out. care in, care out. the law runs both directions.

by port time it was past the wall — ~30,000 lines. any model that touched it had to swallow the whole tree before it could help. the assists were getting noisier, not better. the framework had also become a default i'd never re-examined. time to test both in the same pass.

purpose

two questions. is a 1:1 port across frameworks even possible — same routes, same sketches, same visual output, zero regressions? and if it is, how good can it get?

the Garden made a clean test: canvas-heavy and imperative by nature. 31 generative sketches, WebGL cloud shaders, voronoi images, metaball simulations, particle-text effects, a three.js scene. React's value is diffing a tree and batching updates; canvas and WebGL bypass the DOM entirely. the component tree was already mostly useEffect hooks pretending to be declarative. clean test for whether a better-fit framework shows up in the numbers.

threesam.com home page — clouds background with gallery strip below
/ — clouds + the gallery strip. virtualized canvas tiles.
threesam.com /self page — voronoi-treated portrait and essay
/self — voronoi-treated portrait, essay underneath.
threesam.com /anything-but-analog page — particle text hero and sketch index
/anything-but-analog — particle text hero, 31 sketches behind it.
threesam.com /deana page — D-ANA hero with message timeline
/deana — D-ANA hero, message timeline, word cloud.
threesam.com /shelf page — book grid from Goodreads
/shelf — book grid via Goodreads.
threesam.com /benny page — 102 Jackson Street tribute with video hero
/benny — 102 jackson street tribute, podcast video, playlists.

findings

it's possible. and it landed higher than the baseline.

lighthouse perf, per route — next.js → sveltekit
0255075100/+35/deana+28/sounds+26/anything-but-analog+23/shelf+15/dad+14/canvas/self+8/thoughts+5/benny+2
next.js sveltekit

next.js live-production baseline vs sveltekit with perf work applied. a11y / best practices / seo: 100 / 99.6 / 100 — held across both. /canvas/self is the outlier at 70 — a WebGL-canvas ceiling, not a framework issue.

the port was a pruning pass too. one commit deleted 5,557 lines: dead audio code, orphan canvases, deprecated route stubs, duplicate libs shadowing each other across the old and new src/ trees. the codebase landed at ~60% of its original size — back below the wall. the model can hold the whole thing in its head again.

the component shape got simpler too. canvas logic that had been jammed into useEffect cleanup cycles — stale refs, double-mounts in strict mode, cleanup ordering — moved into Svelte actions. an action is just: here's a node, do something to it, here's how to undo it. that's the exact shape the sketches were already written in. the friction disappeared.

driven by an AI agent. 78 commits, multi-day window, ~5–6 hours of active code transformation; the rest was spec, plan, visual diff, perf work — the human judgment loop. total LLM cost: tens of dollars.

at peak, 37 sub-agents ran in parallel. each one got one tiny, scoped chunk — small enough to spec and review independently. i call it the review train: small offshoot, heavy review, merge, next chunk. no chunk was ever the whole codebase, so no agent ever hit the wall.

guardrails are why it worked — right docs piped in (Svelte MCP for the new framework, the existing repo as the source of truth for the old), methodology spec'd up front, tight per-chunk reviews. agents handed loose instructions vibe-code. agents handed scoped chunks under heavy review ship code you can defend.

framework MCP servers killed the old "Next.js wins because the model knows it best" argument. the model can read any framework's source of truth on demand. defaults that compounded for years are just defaults now.

this isn't a svelte-beats-react take. you can read what an app actually does and pick accordingly. sometimes the answer is Next.js. here it wasn't.

the work is public: #29 (port), #30 (perf), #31 (content-prerender hotfix), #33 (route rename).