WordPress to Astro: How We Hit Lighthouse 100 on Our Rebuild
Let me be honest with you: our old WordPress site was embarrassing. Not because it looked bad — it actually looked pretty decent. But under the hood? A 3.2-second Time to Interactive, a Lighthouse performance score hovering around 58, and a stack of plugins that made every deploy feel like defusing a bomb. We're a web development agency. We build fast sites for clients. And our own site was... not fast.
So we tore it down and rebuilt it with Astro. The result: perfect 100s across all four Lighthouse categories — Performance, Accessibility, Best Practices, and SEO. Not on a single page. On every page. This is the story of how we got there, what broke along the way, and what we'd do differently.
Table of Contents
- Why We Left WordPress
- Why We Chose Astro
- The Migration Strategy
- Architecture Decisions That Made the Difference
- Performance Optimizations Deep Dive
- The Lighthouse 100 Scorecard
- Before and After: The Numbers
- What We Got Wrong Along the Way
- Lessons for Your Own Migration
- FAQ

Why We Left WordPress
Look, WordPress powers something like 43% of the web. It's not a bad platform. We've built plenty of WordPress sites for clients and will continue to do so when it's the right fit. But for our own agency site — a mostly-static marketing site with a blog — WordPress was overkill in the worst way.
Here's what our WordPress setup looked like:
- Theme: Custom theme built on Sage (Roots.io)
- Plugins: 14 active plugins including Yoast SEO, WP Rocket, Advanced Custom Fields Pro, Gravity Forms, and a handful of others
- Hosting: WP Engine Professional plan at $60/month
- CDN: Cloudflare Pro at $20/month
- Build complexity: PHP templating, Webpack for assets, MySQL database
Even with WP Rocket doing aggressive caching, our Core Web Vitals were mediocre. The Largest Contentful Paint (LCP) was 2.4 seconds on mobile. Cumulative Layout Shift (CLS) was 0.12 — not terrible, but not good. And every time we updated a plugin, there was a nonzero chance something would break.
The real kicker? We were paying $80/month in hosting costs for a site that got maybe 3,000 visits a month. That's not a lot of traffic, and that's a lot of money for what was essentially a brochure site with a blog.
The Breaking Point
The final straw came in January 2025. A WordPress core update broke our custom Gutenberg blocks. Fixing it required updating ACF Pro, which required updating our theme's PHP version compatibility, which required updating the hosting environment. What should have been a routine update turned into a full day of work.
I looked at our team and said, "We tell clients to go headless. Why aren't we eating our own cooking?"
Why We Chose Astro
We evaluated four options for the rebuild:
| Framework | Pros | Cons | Our Verdict |
|---|---|---|---|
| Next.js | We know it well, great ecosystem | Overkill for a content site, requires server or edge runtime | Too heavy |
| Astro | Content-focused, ships zero JS by default, island architecture | Smaller ecosystem, newer | Perfect fit |
| Eleventy | Simple, fast builds, mature | Limited component model, less modern DX | Close second |
| Hugo | Blazing fast builds, single binary | Go templating is painful, limited flexibility | Not for us |
We build a lot of Next.js projects for clients, and it's our go-to for anything with dynamic functionality. But for a content-heavy marketing site? Next.js ships a JavaScript runtime whether you need it or not. Even with static export, you're sending React to the browser.
Astro's philosophy resonated with us: ship HTML, add JavaScript only where you need it. Their island architecture means you can have a fully interactive React component sitting next to completely static HTML, and the static parts ship zero JavaScript. That's exactly what we needed.
We'd also been doing more Astro development work for clients throughout 2024, so the team was comfortable with the framework. It wasn't a learning exercise — it was a tool we already trusted.
The Content Layer Question
One decision we made early: we weren't going to use a headless CMS for our own site. For client projects, we often recommend headless CMS setups with Contentful, Sanity, or Storyblok. But for our blog, where every author is a developer comfortable with Markdown and Git? Content Collections in Astro with MDX files committed to the repo was simpler and faster.
No API calls at build time. No content delivery network for content. No extra service to manage or pay for. Just files in a folder.
The Migration Strategy
We didn't do a big-bang migration. Here's our phased approach:
Phase 1: Content Audit (1 week)
We exported all WordPress content using wp-cli and converted posts to MDX using a custom script built with turndown (HTML-to-Markdown converter) plus some regex cleanup. We had 47 blog posts at the time. About 12 of them were outdated or low-performing, so we redirected those to relevant newer content and didn't migrate them.
Phase 2: Design System in Astro (2 weeks)
We rebuilt our component library as Astro components. Buttons, cards, section layouts, navigation — all as .astro files. No framework needed for any of them. Pure HTML and CSS with scoped styles.
Phase 3: Page Build (2 weeks) Home page, capabilities pages, about, contact, blog listing, individual blog posts, 404. We built them all as Astro pages with our component library.
Phase 4: Performance Tuning (1 week) This is where the Lighthouse 100 work really happened. More on this below.
Phase 5: Launch and Redirect (2 days) We set up proper 301 redirects for every old URL, verified with Screaming Frog that nothing was broken, submitted the new sitemap to Google Search Console, and flipped DNS.
Total timeline: about 6 weeks of part-time work alongside client projects.

Architecture Decisions That Made the Difference
Zero JavaScript by Default
Our entire site ships with approximately 2KB of JavaScript total. That's not a typo. Two kilobytes. And most of that is a small script for mobile navigation toggle and analytics.
Here's our mobile nav — no framework, no dependencies:
---
// MobileNav.astro
---
<button id="menu-toggle" aria-expanded="false" aria-controls="mobile-menu">
<span class="sr-only">Toggle menu</span>
<svg><!-- hamburger icon --></svg>
</button>
<nav id="mobile-menu" hidden>
<slot />
</nav>
<script>
const toggle = document.getElementById('menu-toggle');
const menu = document.getElementById('mobile-menu');
toggle?.addEventListener('click', () => {
const expanded = toggle.getAttribute('aria-expanded') === 'true';
toggle.setAttribute('aria-expanded', String(!expanded));
menu?.toggleAttribute('hidden');
});
</script>
That <script> tag in an Astro component gets bundled and deduplicated automatically. It's tiny, it's vanilla JS, and it works everywhere.
CSS Strategy: Scoped Styles + A Minimal Global Layer
We used Astro's built-in scoped CSS for component-level styles and a single global stylesheet (about 8KB minified) for typography, reset, custom properties, and utility classes. No Tailwind. Controversial take, I know.
We love Tailwind for larger applications and client projects. But for a site this small, it added build complexity and file size we didn't need. Our hand-written CSS is smaller than Tailwind's output would have been, even with purging.
/* Global custom properties */
:root {
--color-text: #1a1a2e;
--color-bg: #ffffff;
--color-accent: #e94560;
--color-accent-dark: #c81e45;
--font-body: 'Inter', system-ui, sans-serif;
--font-heading: 'Cal Sans', var(--font-body);
--max-width: 72rem;
--space-unit: 0.25rem;
}
Static Generation with Smart Preloading
Every page is statically generated at build time. We use Astro's built-in prefetch integration to preload links on hover, making navigation feel instant:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://socialanimal.dev',
integrations: [mdx(), sitemap()],
prefetch: {
prefetchAll: false,
defaultStrategy: 'hover',
},
build: {
inlineStylesheets: 'auto',
},
});
Performance Optimizations Deep Dive
Getting to Lighthouse 100 isn't just about choosing the right framework. Astro gives you a head start, but the last 10-15 points require deliberate effort. Here's what we did.
Image Optimization
Astro's built-in <Image /> component handles responsive images with automatic format conversion (WebP/AVIF), lazy loading, and proper width/height attributes to prevent CLS.
---
import { Image } from 'astro:assets';
import heroImage from '../assets/hero.jpg';
---
<Image
src={heroImage}
alt="Social Animal development team working on headless architecture"
widths={[400, 800, 1200]}
sizes="(max-width: 600px) 400px, (max-width: 900px) 800px, 1200px"
format="avif"
fallbackFormat="webp"
quality={80}
loading="eager"
/>
For the hero image specifically, we use loading="eager" since it's above the fold. Everything else gets loading="lazy" by default.
We also went through every image on the site and asked: "Does this need to be an image?" Several decorative elements became CSS gradients or SVGs instead. Our hero section background, for example, is a CSS gradient with a subtle noise texture applied via a tiny inline SVG.
Font Loading Strategy
Fonts are a Lighthouse killer. Here's our approach:
Self-host everything. No Google Fonts CDN. We downloaded Inter and Cal Sans and serve them from our own domain. That eliminates a DNS lookup, TCP connection, and TLS handshake to fonts.googleapis.com.
Subset aggressively. We used
glyphhangerto analyze which characters we actually use, then subset our fonts withpyftsubset. Our Inter Regular WOFF2 went from 96KB to 18KB.Use
font-display: swapwith a carefully chosen system font fallback that matches metrics closely, minimizing layout shift during swap.Preload the critical font files:
<link rel="preload" href="/fonts/inter-latin-400.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="/fonts/cal-sans-latin-600.woff2" as="font" type="font/woff2" crossorigin />
Hosting: Cloudflare Pages
We moved from WP Engine ($60/month) to Cloudflare Pages (free tier). Yes, free. Our site is well within the limits of Cloudflare's free plan — 500 builds per month, unlimited bandwidth, unlimited requests.
Cloudflare Pages deploys from a Git push, serves from their global edge network, and handles cache headers automatically. Build times average 22 seconds for our entire site. Compare that to our WordPress setup where a cache purge alone could take longer.
Monthly hosting cost went from $80 to $0.
Critical CSS Inlining
Astro automatically inlines small stylesheets when you set build.inlineStylesheets: 'auto'. For our pages, every critical style is inlined in the <head>, meaning there are zero render-blocking CSS requests. The browser can start painting immediately.
Third-Party Script Discipline
This is where most sites lose their perfect scores. Every third-party script is a potential performance disaster. We ruthlessly limited ours:
- Analytics: Switched from Google Analytics (70KB+ of JavaScript) to Plausible Analytics (< 1KB script, loaded async). We pay $9/month for Plausible, and the data quality is honestly better for our needs.
- Forms: Our contact form at /contact uses a simple HTML form with server-side handling via Cloudflare Pages Functions. No JavaScript form library.
- No chat widgets. No social media embeds. No cookie consent banners (we don't use cookies that require consent).
The Lighthouse 100 Scorecard
Here are our actual Lighthouse scores as of May 2025, measured using Chrome DevTools on a throttled connection (Lighthouse default mobile simulation):
| Metric | Score |
|---|---|
| Performance | 100 |
| Accessibility | 100 |
| Best Practices | 100 |
| SEO | 100 |
| First Contentful Paint | 0.6s |
| Largest Contentful Paint | 0.8s |
| Total Blocking Time | 0ms |
| Cumulative Layout Shift | 0 |
| Speed Index | 0.8s |
The Total Blocking Time of 0ms is my favorite stat. Zero. There's essentially no JavaScript blocking the main thread. Ever.
Before and After: The Numbers
| Metric | WordPress (Before) | Astro (After) | Improvement |
|---|---|---|---|
| Lighthouse Performance | 58 | 100 | +72% |
| LCP (mobile) | 2.4s | 0.8s | 3x faster |
| CLS | 0.12 | 0 | Eliminated |
| TBT | 380ms | 0ms | Eliminated |
| Page weight (home) | 1.8MB | 142KB | 92% smaller |
| HTTP requests | 47 | 6 | 87% fewer |
| JavaScript shipped | 340KB | 2KB | 99.4% less |
| Monthly hosting cost | $80 | $9 (Plausible only) | 89% cheaper |
| Build/deploy time | 3-5 min | 22 sec | ~10x faster |
| Time to first byte | 420ms | 18ms | 23x faster |
The page weight reduction is staggering even to us. 1.8MB down to 142KB. That's what happens when you stop shipping jQuery, React, WP Rocket's script loader, Yoast's schema markup injector, and fourteen plugin stylesheets.
What We Got Wrong Along the Way
It wasn't all smooth sailing. Honesty time.
Mistake 1: We Almost Over-Engineered Content Management
Our first instinct was to set up Sanity as a headless CMS for the blog. We spent two days configuring schemas and setting up the Sanity Studio before stepping back and asking, "Who is actually going to use this?" The answer was... us. Developers. Who are perfectly happy writing MDX in VS Code. We ripped out Sanity and went with Astro Content Collections. Saved ongoing costs and complexity.
Mistake 2: Font Subsetting Broke Special Characters
Our initial font subset was too aggressive. We stripped out characters we thought we'd never use, then published a blog post with an em dash and a few curly quotes that rendered as boxes. Lesson: test your subsets with real content, not just "ABCDEFG."
Mistake 3: We Forgot About OpenGraph Images
We launched without dynamic OG images. When someone shared a blog post on Twitter/X or LinkedIn, it showed a generic fallback. We had to go back and build an OG image generation pipeline using @astrojs/og (which uses Satori under the hood). Should've been in the original scope.
Mistake 4: The 301 Redirect Map Had Gaps
Despite using Screaming Frog to map old URLs, we missed a handful of image URLs that external sites were hotlinking to. We caught these in Cloudflare's analytics about a week after launch and added the missing redirects. Always check your server logs after a migration — Google Search Console won't catch everything.
Lessons for Your Own Migration
If you're considering moving from WordPress to a static-first framework, here's what I'd tell you:
Audit before you migrate. Kill content that isn't performing. A migration is a great opportunity to prune.
Match the tool to the job. Astro was perfect for us because we're mostly content. If you need heavy interactivity, Next.js or a similar framework might be the better call.
Don't cargo-cult your old architecture. We didn't try to replicate our WordPress setup in Astro. We rethought everything from scratch. Do we actually need a form plugin? No, a
<form>element with a serverless function works fine.Measure before, measure after, measure continuously. We set up a Lighthouse CI job in GitHub Actions that runs on every pull request. If a PR drops any score below 95, it fails the check.
Budget for the "last 5%." Getting from Lighthouse 85 to 95 is straightforward. Getting from 95 to 100 requires font subsetting, critical CSS analysis, image format optimization, and third-party script auditing. Plan time for it.
Your hosting costs should embarrass your old setup. If you're serving static files and still paying significant hosting fees, something is wrong. Static hosting is a commodity now.
If you're interested in what a migration like this would look like for your project, check out our pricing page or get in touch. We've done this migration path for several clients now, and the performance gains are consistently dramatic.
FAQ
How long does it take to migrate a WordPress site to Astro? For our site (about 50 pages including blog posts), it took roughly 6 weeks of part-time work. A larger site with hundreds of posts and complex custom post types could take 8-12 weeks. The actual development is usually faster than the content audit and redirect mapping.
Can you get Lighthouse 100 with Next.js instead of Astro? It's possible but significantly harder. Next.js ships a JavaScript runtime to the browser even for static pages (the React hydration layer). You can get close — scores of 95-99 are achievable with careful optimization. But Astro's zero-JS-by-default approach makes perfect scores much more attainable for content sites.
What about WordPress features like contact forms and search? Contact forms work fine with plain HTML forms and a serverless function backend (Cloudflare Pages Functions, Netlify Functions, etc.). For search, we use a client-side search with Pagefind, which builds a search index at build time and ships only 5KB of JavaScript. It's fast and works offline.
Does migrating from WordPress to Astro hurt SEO? Not if you handle it properly. We set up 301 redirects for every URL, maintained our URL structure where possible, submitted a new sitemap, and kept all our structured data. Our organic traffic actually increased 23% in the three months after migration, likely due to improved Core Web Vitals.
How do you handle dynamic content like comments on an Astro site? We don't have comments on our blog — they were mostly spam on WordPress anyway. If you need comments, services like Giscus (GitHub Discussions-based) or Hyvor Talk work well as embedded components. They load as Astro islands, so they don't affect initial page load.
Is Astro production-ready for large sites? Absolutely. Astro 5.x (released late 2024) is mature and stable. Companies like Porsche, Google, Microsoft, and Netlify use it in production. The build performance scales well too — sites with thousands of pages build in under a minute with the right configuration.
What's the ongoing maintenance like compared to WordPress? Dramatically less. No plugin updates, no database maintenance, no security patches for PHP. We update Astro and its dependencies maybe once a month via Dependabot PRs. Each update takes about 5 minutes to review and merge. Compare that to the WordPress update treadmill.
Can non-technical team members still edit content on an Astro site? With our setup (MDX files in Git), you need to be comfortable with Markdown and basic Git workflows. For teams with non-technical editors, we recommend pairing Astro with a headless CMS like Sanity, Contentful, or Storyblok. The editors get a visual interface, and you still get all the performance benefits of static generation.