From WordPress to Custom React: How I Cut Load Time by 70%
I used to run my blog on WordPress. It was easy to set up, had a thousand plugins, and my mom could probably figure out the dashboard.
Md. Rony Ahmed
· 10 min read
From WordPress to Custom React: How I Cut Load Time by 70%
I used to run my blog on WordPress. It was easy to set up, had a thousand plugins, and my mom could probably figure out the dashboard.
It also took 4.2 seconds to load the homepage. On a fast connection.
The final straw wasn't the speed — it was a plugin update that broke my contact form for three days before I noticed. A potential client filled out the form, got a 500 error, and moved on. I lost a $300 project to a PHP fatal error I didn't even know existed.
That's when I decided to rebuild everything from scratch. Not because I'm allergic to WordPress, but because I needed to know exactly what every line of code did.
The new stack loads in 0.8 seconds. Here's the migration and what I learned.
The Old Stack (And Why It Failed Me)
WordPress + Elementor + Shared Hosting
| Metric | Before |
|---|---|
| Homepage load | 4.2s |
| Time to Interactive | 6.1s |
| PageSpeed score | 42/100 |
| Plugin count | 23 |
| Database queries per page | 187 |
| Monthly cost | $15 |
The problems weren't theoretical:
Plugin roulette. Every update was a coin flip. Will this break my navigation? Will Elementor's latest patch conflict with my caching plugin? I spent more time troubleshooting WordPress than writing.
Shared hosting sharing. My "unlimited" plan meant I was on a server with 400 other websites. One of them got a traffic spike and my database queries started timing out.
Bloated pages. Elementor is powerful, but it generates DOM trees that would make a redwood forest jealous. My homepage had 847 DOM elements. Google's recommended maximum is 1,500 for an entire page.
The database was a mess. Years of plugin installs left orphaned tables, revision history eating 80MB, and transients that never expired. I had 12MB of actual content and 340MB of database bloat.
The New Stack
React + Vite + Tailwind CSS + Supabase + Vercel
| Metric | After | Improvement |
|---|---|---|
| Homepage load | 0.8s | **81% faster** |
| Time to Interactive | 1.2s | **80% faster** |
| PageSpeed score | 97/100 | **+131%** |
| JavaScript bundle | 78KB gzipped | **Custom** |
| Database queries per page | 2-4 | **98% fewer** |
| Monthly cost | $0 | **Free** |
The monthly cost drop isn't a flex — it's a side effect of modern tooling. Vercel's hobby tier handles my traffic. Supabase's free tier covers my database. The only thing I pay for is my domain.
Why I Didn't Choose Next.js
This is the question I get most often. Everyone assumes a React blog means Next.js.
I considered it. Here's why I went with Vite instead:
I don't need SSR. This is a content blog, not a dashboard. Every post is static at deploy time. Vite's static site generation is simpler, faster to build, and produces cleaner output.
Build speed matters more than features. Vite cold builds in 3 seconds. Next.js takes 15-30 seconds for the same content. When I'm iterating on a post and want to preview changes, that difference adds up.
Dependency overhead. Next.js pulls in 47 dependencies for a basic app. Vite needs 12. Fewer dependencies means fewer audit warnings, smaller attack surface, and faster
npm install.I control the routing. Next.js file-based routing is convenient until you want something unconventional. Vite lets me define routes explicitly in code. It's more verbose but I always know exactly what's happening.
The tradeoff: I lose automatic image optimization and API routes. I solved images with a build-time sharp script. I don't need API routes — Supabase handles all my backend needs.
The Migration Process (Step by Step)
This wasn't a weekend project. It took two weeks of evenings, mostly because I kept getting distracted by optimization rabbit holes.
Week 1: Content Export and Cleaning
Step 1: Extract content from WordPress
WordPress's export XML is bloated. I wrote a Python script to parse it and strip out Elementor markup, shortcodes, and plugin-generated HTML.
The script took 23 WordPress posts and produced clean Markdown. But it also revealed how much garbage Elementor had injected:
- 847
div wrappers with Elementor class names- 34 inline style blocks
- 12 instances of
data-elementor-type attributes- 6 embedded Font Awesome icon scripts
I deleted all of it. The posts lost their fancy layouts, but gained readability. Content should be content, not a styling system.
Step 2: Rewrite URLs and fix images
WordPress stored images in
wp-content/uploads/2024/03/. I moved everything to /images/posts/ with descriptive filenames. IMG_4732.jpg became postgres-connection-pooling-diagram.webp.This took six hours but was worth it. Descriptive filenames help SEO, and WebP conversion cut image sizes by 60% on average.
Step 3: Audit and categorize
WordPress had 47 tags. I consolidated to 12 meaningful ones. Tags aren't decoration — they're navigation. If a reader clicks "Redis," they should find every Redis post, not half of them because the other half was tagged "cache."
Week 2: Build and Deploy
Step 4: Design the component system
I built five core components:
1.
PostCard — Blog listing cards with lazy-loaded images2.
PostContent — Markdown renderer with syntax highlighting3.
TagCloud — Weighted tag navigation4.
RelatedPosts — Tag-matching recommendations5.
NewsletterForm — Email capture with Supabase backendEverything else is composition of these five. No third-party UI libraries. No bloated component frameworks.
Step 5: Performance budget enforcement
I set hard limits in my build pipeline:
- Total JS bundle: <100KB gzipped
- First image: load within 500ms
- Largest Contentful Paint: <1.5s
- Cumulative Layout Shift: <0.1
If a build violates any of these, it fails. This prevents the gradual performance death that killed my WordPress site.
Step 6: Database schema design
Supabase
posts table:CREATE TABLE posts (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
slug text UNIQUE NOT NULL,
title text NOT NULL,
excerpt text NOT NULL,
content text NOT NULL,
cover_image text,
category text NOT NULL,
tags text[] DEFAULT '{}',
reading_time integer,
published_at timestamptz,
is_featured boolean DEFAULT false,
created_at timestamptz DEFAULT now()
);
CREATE INDEX idx_posts_published ON posts(published_at DESC);
CREATE INDEX idx_posts_category ON posts(category);
CREATE INDEX idx_posts_tags ON posts USING GIN(tags);
Notice the indexes.
published_at DESC makes the homepage query instant. GIN on tags makes related-post matching fast. WordPress had none of these optimizations — it was scanning 187 rows every page load.Step 7: Deploy and monitor
Vercel deploys on every git push. I added three monitoring checks:
1. Lighthouse CI — runs on every PR, blocks if score drops below 95
2. Bundle analyzer — warns if any dependency adds >10KB
3. Uptime monitoring — ping every 5 minutes, alert on 2 failures
The Performance Breakdown
Here's exactly where the 70% improvement came from:
| Optimization | Impact |
|---|---|
| Static generation (no server rendering) | -1.2s |
| WebP images + lazy loading | -0.9s |
| Minimal JavaScript (78KB vs 890KB) | -0.6s |
| Database indexes (2 queries vs 187) | -0.4s |
| No plugin CSS/JS bloat | -0.3s |
| **Total** | **-3.4s (81%)** |
The biggest single win was static generation. WordPress runs PHP, queries MySQL, and assembles the page on every request. My site is pre-built HTML served from a CDN edge node. The server does zero work per request.
What I Miss About WordPress
Let's be fair. Not everything was worse:
The media library. Drag, drop, done. Now I write a Python script to optimize, convert, and upload images. It's faster once set up, but there's no GUI for my girlfriend to use.
Yoast SEO. I built my own metadata generator, but Yoast's real-time feedback was nice. My system works, but it's less forgiving if I forget a meta description.
The ecosystem. Need a contact form? WordPress has 50 plugins. I had to build mine in an afternoon. It's leaner and does exactly what I need, but it cost me time.
One-click staging. Vercel previews every PR automatically, which is arguably better, but WordPress's staging clone was easier to explain to non-developers.
What I Don't Miss At All
Plugin updates. I haven't had a mysterious white-screen-of-death since the migration. My build either passes or fails predictably.
Database bloat. Supabase is at 14MB for the same content that needed 340MB in WordPress. No revisions, no transients, no orphaned tables.
Shared hosting neighbors. Vercel's edge network serves from the closest of 100+ locations. My site loads faster in Tokyo than my old shared hosting loaded in Toronto.
SEO plugin upsells. "Upgrade to Pro for redirects!" I wrote a 15-line redirect handler in my router. Free forever.
Should You Migrate Too?
Yes, if:
- Your site is content-focused (blog, portfolio, documentation)
- You're comfortable writing React components
- Performance scores matter to you (clients, employers, pride)
- You spend more time fixing WordPress than writing content
No, if:
- You need complex user roles and permissions
- Non-technical people update content regularly
- You rely on specific plugins (membership systems, booking engines)
- You value convenience over control
Maybe, if:
- You have 100+ posts. The migration effort scales with content volume. My 23 posts took two weeks. A 500-post site might take two months.
- You use advanced WordPress features. Custom post types, taxonomies, and ACF fields need equivalent structures in the new stack.
The Code That Made It Possible
My
vite.config.ts — the entire build configuration:import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { globSync } from 'glob'
// Find all Markdown posts and generate routes
const posts = globSync('content/posts/**/*.md')
const routes = posts.map(file => {
const slug = file.replace('content/posts/', '').replace('.md', '')
return `/blog/${slug}`
})
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
input: {
main: 'index.html',
...routes.reduce((acc, route) => {
acc[route] = `src/entries${route}.html`
return acc
}, {})
}
}
}
})
That's it. No complex framework config. No hidden magic. Just explicit routes pointing to explicit files.
Results After 3 Months
The migration happened in March. Here's what changed:
| Metric | Before | After | Change |
|---|---|---|---|
| Average session duration | 1:42 | 2:38 | **+55%** |
| Bounce rate | 68% | 41% | **-40%** |
| Pages per session | 1.8 | 3.2 | **+78%** |
| Newsletter signups/mo | 12 | 47 | **+292%** |
| Contact form submissions | 3/mo | 11/mo | **+267%** |
Speed doesn't just feel better — it converts better. Readers who don't wait for pages to load read more pages. People who read more pages subscribe more often. Subscribers become clients.
That $300 project I lost to a broken form? I've landed three since the migration. Two found me through search, one through a newsletter signup. The performance investment paid for itself in the first month.
What's Next
The stack isn't done evolving. Two experiments in progress:
Edge caching with Vercel KV. Currently regenerating related posts on every deploy. Moving to cached recommendations that update incrementally.
AI-assisted content formatting. Building a pipeline that suggests heading structure, reading time estimates, and tag recommendations based on draft content. Not replacing my writing — augmenting it.
The Bottom Line
WordPress is fine for what it is. But "fine" wasn't enough for me. I wanted predictable performance, clean code I fully understood, and zero surprise failures from plugin updates.
The custom stack costs me $0/month, loads in 0.8 seconds, and hasn't broken once in three months of daily deployments.
That's not a knock on WordPress. That's just what happens when you build exactly what you need and nothing more.
Want to see the site? [codehustle.tech](https://codehustle.tech) — built with everything described above.
Questions about the migration? Drop them below. I documented the entire process because I wished someone had documented it for me.