Skip to content
Nam
Go back

Rebuilding my wife's website, Part I: WordPress → static HTML — and the honest pros and cons

WordPress to static HTML, Part I — first paint 10.3s to 1.7s, JavaScript 700KB to 5KB, requests 122 to 44

My wife Krystle (張可澄) is a professional bilingual MC in Hong Kong — five thousand-plus events, a Hong Kong MC Competition championship, the works. Her portfolio site, krystle.hk, is how new clients find her.

Last weekend I finally looked under its hood. It ran WordPress + Elementor + a heavy theme, scored 56 on Lighthouse with a 13.8-second largest paint — and hid one genuinely painful bug that deserves its own post (that’s Part II).

One Sunday evening with Claude Code later: the whole thing is rebuilt as 102 pages of dependency-free static HTML. Same design, same URLs. First paint 1.7 s, ~5 KB of JavaScript, zero broken links.

This is Part I — the rebuild itself, and an honest look at when leaving WordPress is the right trade (and when it isn’t).

The original WordPress homepage, full page

The original WordPress homepage…

The rebuilt static homepage — visually the same site

…and the rebuild. Visually the same site — that was the brief. Everything underneath changed.

Table of contents

Open Table of contents

The audit: what was actually being shipped

First step: crawl the sitemap, download all 101 pages and 2,186 asset files (223 MB), screenshot everything, and run a throttled Lighthouse baseline.

The homepage alone shipped 20 CSS files, 70 JS files — 122 requests, 2.2 MB. That included React, ReactDOM and lodash on every page… loaded by WooCommerce, for a shop with zero products. And Contact Form 7, for a site with zero forms. Nobody had chosen any of this; it’s just what accumulates when a theme, a page builder and a decade of plugins each bring their friends.

Fonts were their own small disaster: 872 KB across 17 requests — every weight of Noto Sans TC, Montserrat and Roboto, mostly unused.

The bug I’m saving for Part II

The audit also found that a broken “Load More” button had been silently hiding 76 of her 85 events from every visitor. That story — and how the interface was rebuilt so it can’t happen again — is Part II: the UI.

The plan: same site, a hundredth of the code

The brief was strict on purpose. Same look. Same pages. Same URLs — nothing 404s, the Google index survives. No redesign. Just take everything the WordPress stack did with plugins, and do it with almost nothing:

WordPress (heavy)Rebuild (light)
Slider Revolution hero (~700 KB JS)one composed image + CSS-positioned text layers
jQuery + GSAP scroll animations~30 lines of IntersectionObserver + CSS transitions
Isotope filter + AJAX “Load More”all 85 cards in the DOM; filter = show/hide — no network
Magnific Popup lightboxnative <dialog> + ~40 lines of JS
Google Fonts: 17 requests, 872 KBself-hosted subsets: 5 files, 392 KB
WooCommerce / CF7 / Instagram pluginsdeleted; junk URLs 301’d

(What each of those replacements looks and feels like — the hero, the filter, the lightbox, mobile — is Part II’s territory.)

The build

The architecture is one idea: hand-written templates + a small Node build script + JSON data files. Python scripts parsed the downloaded WordPress HTML into clean JSON — nav, hero, testimonials, the 84-logo client wall, and per-event data. Then build.mjs (no framework, ~400 lines) stamps out all 102 pages, and a sharp pipeline generated 805 image derivatives (WebP grids, two gallery sizes, og:images) from the 397 originals.

The whole site now ships ~19 KB of CSS and ~5 KB of JavaScript — less interactive code than the old site’s smallest single plugin file. The CJK trick mattered most: full Noto Sans TC is enormous, so the build subsets it to the 919 glyphs the site actually uses (272 KB, variable weight axis intact).

A debugging war story

The phantom 6.5-second LCP. After the rebuild, Lighthouse’s simulated-throttling mode insisted LCP was 6.5s with a five-second “render delay”. Its own filmstrip showed the hero fully painted at 1.9s, and a real DevTools-throttled trace (4× CPU, Slow 4G) measured LCP at 207 ms. Hours of variant-testing — killing animations, inlining CSS, swapping test servers (fun fact: Python’s http.server speaks HTTP/1.0 with no keep-alive, which distorts the simulator further) — proved it was a measurement artifact.

Don’t optimize a number you haven’t cross-examined

Simulator said 6.5s. Filmstrip said 1.9s. Real trace said 0.2s. When a metric contradicts the pixels, check the filmstrip and a real trace before “fixing” anything — the score was lying, the page wasn’t.

(There was a second war story — a font whose kerning data mangled Krystle’s own surname. It’s a UI bug, so it lives in Part II.)

The results

Metric (throttled mobile)WordPressStatic rebuild
First Contentful Paint10.3 s1.7 s
Speed Index10.3 s2.5 s
Real-trace LCP (4× CPU, Slow 4G)~0.2 s
Homepage requests122~44
Fonts872 KB · 17 req · third-party392 KB · 5 req · self-hosted
JavaScript shipped~700 KB incl. React + jQuery~5 KB vanilla
Lighthouse Accessibility94100
Lighthouse Best Practices96100

(The SEO column — 85 → 100 — gets its own scoreboard in Part III.)

QA before calling it done: all 815 internal URLs across the 102 pages checked — zero broken; filter, load-more and lightbox exercised in a real browser; desktop and mobile visually diffed against the original’s screenshots.

Then came the boring-but-scary step — full WordPress backup, then the DNS flip to Cloudflare. krystle.hk is live on the rebuild, so the referee I deferred to earlier got its say:

PageSpeed Insights on the live rebuilt site, mobile: Performance 90, Accessibility 100, Best Practices 100, SEO 100

Live PSI, mobile: 90–95 depending on the run (PSI wobbles a few points) — 0 ms blocking time, 0.005 layout shift. The old site scored 57 on the same test hours earlier.

PageSpeed Insights on the live rebuilt site, desktop: 100 across all categories, FCP 0.3s, LCP 0.6s

Desktop: a straight 100 across the board — first paint 0.3 s, LCP 0.6 s. And the phantom 6.5-second LCP? Live mobile LCP is 2.9–3.7 s, desktop 0.6 s. The artifact stayed in the lab.

The honest pros and cons

This is the part most “we left WordPress” posts skip. The trade is real, in both directions.

What we gained:

What we gave up:

The actual decision rule

Static-from-data fits a site whose content is structured and changes in batches — portfolios, brochures, menus, event lists. It’s the wrong answer for a non-technical owner who writes daily, or a site that lives off plugins. The question isn’t “is WordPress bad?” — it’s who edits this site, how often, and in what shape?

What this actually demonstrates

This is the same story I keep telling clients about AI adoption, just closer to home. The agent did the parts that never get done because they’re tedious: crawling every page, generating 805 image variants, stamping 102 pages, checking 815 links. I did the parts that need taste and judgment: what to keep, what to cut, when “the score is lying” — and the final say on my wife’s public face.

Neither of us could have shipped this in a weekend alone. That’s the whole point.


This series: Part I — you’re here · Part II — the UI: same design, an interface that works · Part III — the SEO story · Part IV — the site was live, then came the lessons.

Got a site that’s slower than it should be? I’m happy to take a look — email me.


Share this post

Choose how you’d like to share it.


Previous Post
Rebuilding my wife's website, Part II: the UI — same design, an interface that finally works
Next Post
Vibe-coding NamLoop: an ad-free YouTube looper, shipped in a weekend