From vanilla to React, and what it actually cost | WaretaGarasuSkip to main content
WaretaGarasu
Language
Theme
Glass
All writingNote · Architecture

From vanilla to React, and what it actually cost

I moved this site from hand-rolled HTML, CSS, and JavaScript to a React 19 SPA. It got heavier, not lighter. Here's why that was still the right call.

React 19ViteArchitectureMigration

Why move at all

The old site was hand-rolled HTML, CSS, and JavaScript. It was fast and I was proud of it. But it had a structural problem: adding a page meant editing a build script, the shared global CSS, the navigation in every file, and the i18n dictionary, all by hand. Every new page made the next one a little more expensive.

That's the quiet trap of a no-framework site. The first ten pages are cheap. The eleventh is where you start paying interest on every shortcut you took. I wasn't fighting a performance problem. I was fighting a maintenance one.

What changed

The migration was to Vite and React 19, with react-router-dom for routing. The single biggest change wasn't at runtime. It was that a page became a file again.

// Before: every page was its own HTML file, and "adding a page"
// meant editing the build script, the shared CSS, and the nav by hand.

// After: a page is one lazy-loaded file. The router does the rest.
const NowPage = lazy(() => import('./pages/NowPage'))

<Route path="/now" element={<NowPage />} />
<Route path="/it/now" element={<NowPage />} />

Lazy-loading each route means the browser only downloads the code for the page you're on. The shared shell, the i18n bundle, and the heavy bits (the Leaflet map on /stats) are separate chunks. Adding this writing section was creating two files and four route lines: no build-script edit, no global-CSS surgery.

What it cost

Here is the honest part: the site got heavier. The vanilla version shipped almost no JavaScript. The React SPA ships an entry bundle around 128KB gzipped before it has rendered anything interesting. There's no spinning that as a win on raw bytes.

What those bytes buy: everything is first-party and self-hosted, there are zero third-party requests, the heavy code is split per route, and the whole thing caches immutably after the first visit. The first paint is slower than vanilla; every navigation after it is instant. For a portfolio people browse rather than bounce off, that's worth it. But it is a trade, not a free lunch.

What I kept

Most of the design system came across untouched, because it never lived in JavaScript. The CSS custom properties, the theme tokens, the flat i18n keys, the pre-paint script that prevents a flash of the wrong theme: all of it was already framework-agnostic. The migration was about how pages are assembled, not how they look. That's the payoff of keeping a design system in CSS rather than in components: you can swap the framework underneath it.

What I'd do differently

I'd move sooner. I treated the framework migration as a big, scary rewrite and put it off, when the design system was portable the whole time and the genuinely risky part was small. The lesson I keep relearning: the source-to-build separation I bolted onto the vanilla site was a workaround for a missing build step. The real fix was the build step itself.