Optimizing Single-Page Applications (SPAs) for SEO
SPAs can be invisible to search engines. Learn how to make your React, Vue, or Angular SPA both fast and SEO-friendly.
Single-Page Applications (SPAs) load a JavaScript shell and render content client-side. This creates two problems: slow initial loads (empty HTML until JavaScript executes) and poor SEO (search engines may not execute JavaScript).
The SPA SEO Problem
When Googlebot fetches a traditional SPA:
<!-- What the server sends -->
<!DOCTYPE html>
<html>
<head><title>My App</title></head>
<body>
<div id="root"></div>
<script src="/app.js"></script>
</body>
</html>
Googlebot sees an empty page. While Google can render JavaScript, it:
- Uses a rendering queue (delays by hours to days)
- Has a rendering budget (may not render all pages)
- Can miss dynamic content loaded via API calls
- May time out on heavy JavaScript
Solution 1: Server-Side Rendering (SSR)
Render HTML on the server for each request:
// Next.js SSR (App Router)
export default async function ProductPage({ params }) {
const product = await getProduct(params.id);
return (
<article>
<h1>{product.name}</h1>
<p>{product.description}</p>
</article>
);
}
Pros: Content immediately available to crawlers, better LCP Cons: Server processing per request, higher TTFB than static
Solution 2: Static Site Generation (SSG)
Pre-render pages at build time:
// Next.js SSG
export async function generateStaticParams() {
const products = await getAllProducts();
return products.map(p => ({ id: p.id }));
}
export default async function ProductPage({ params }) {
const product = await getProduct(params.id);
return <ProductDetails product={product} />;
}
Pros: Fastest possible TTFB, best for SEO, cheapest to host Cons: Build times grow with page count, content freshness depends on rebuild frequency
Solution 3: Incremental Static Regeneration (ISR)
Static pages that auto-refresh:
// Revalidate every 60 seconds
export const revalidate = 60;
Pros: Static speed + fresh content Cons: First visitor after expiry gets stale content (while regeneration happens)
Solution 4: Pre-rendering Service
If you can't switch frameworks, use a pre-rendering service:
- Prerender.io — renders pages for crawlers, serves static HTML
- Rendertron — Google's headless Chrome rendering service
User → SPA (client-rendered)
Googlebot → Pre-rendering service → Static HTML
SEO Technical Requirements for SPAs
Meta Tags
Ensure each page has unique meta tags:
// Next.js metadata
export async function generateMetadata({ params }) {
const product = await getProduct(params.id);
return {
title: product.name,
description: product.description,
openGraph: {
title: product.name,
description: product.description,
images: [product.image],
},
};
}
Canonical URLs
<link rel="canonical" href="https://yoursite.com/products/widget" />
Structured Data
<script type="application/ld+json">
{JSON.stringify({
"@context": "https://schema.org",
"@type": "Product",
"name": product.name,
"description": product.description,
"image": product.image,
})}
</script>
Sitemap
Generate a sitemap with all SPA routes:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://yoursite.com/products/widget</loc>
<lastmod>2026-06-13</lastmod>
</url>
</urlset>
Performance + SEO Comparison
| Approach | TTFB | LCP | SEO | Effort |
|---|---|---|---|---|
| Client-only SPA | Fast (empty) | Slow (JS render) | Poor | None |
| SPA + Pre-rendering | Fast (empty) | Slow (JS render) | Good | Low |
| SSR (Next.js, Nuxt) | Moderate | Fast | Excellent | Medium |
| SSG | Fastest | Fastest | Excellent | Low |
| ISR | Fast (cached) | Fast | Excellent | Low |
Monitor Your SPA Performance
SPAs need both performance and SEO monitoring. BadPageSpeed tracks your Lighthouse scores and Core Web Vitals.
Ready to stop wasting ad spend?
Track your landing page performance, monitor Core Web Vitals, and calculate exactly how much slow pages cost you.
Start Free — No Credit Card