Search engines execute JavaScript, but later, at a cost, and not every crawler does it at all. JavaScript SEO is the discipline of making sure content that depends on JS still gets crawled, rendered and indexed reliably. For developers shipping React, Vue or Svelte apps, this is the technical-SEO area most likely to bite you.
How Google processes JavaScript#
Google describes three phases: crawling, rendering and indexing, each with its own queue.
Crawl: fetch HTML → check robots.txt, parse <a href> links, read status code and meta tags
Render: headless, evergreen Chromium executes your JS (queued: seconds, sometimes longer)
Index: the rendered HTML is indexed and its links are queued for crawlingGoogle says a page "may stay on this queue for a few seconds, but it can take longer than that." The older "two waves, days apart" framing overstates the delay for most sites, but the principle still holds: everything that matters should already be in the initial HTML.
- Content in the initial HTML is seen on the first fetch, by every crawler.
- Content that only exists after JS execution depends on rendering succeeding (no timeouts, no blocked resources, no errors).
- Links that only exist after JS are discovered later, which slows crawling of deeper pages.
- Googlebot reads only the first 2MB of the HTML, and each JS and CSS file has its own 2MB limit. Giant inline hydration payloads can push your real content or tags past the cutoff (Crawling & Indexing).
On 2026-03-04 Google removed an outdated section of its docs, saying "using JavaScript to load content is not 'making it harder for Google Search'." That is true for Google. It is not true for everyone else.
AI crawlers usually don't render#
Vercel's December 2024 analysis found that none of the major AI crawlers it measured (GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot, PerplexityBot, Meta-ExternalAgent, Bytespider) executed JavaScript; Google's Gemini (via Googlebot's infrastructure) and Applebot did. If your content only exists after hydration, ChatGPT, Claude and Perplexity may see an empty shell. That makes server rendering a GEO requirement, not only an SEO nicety. Which of those bots you let in at all is covered in AI Crawlers & Bot Access.
Rendering strategies ranked#
| Strategy | Initial HTML | SEO reliability |
|---|---|---|
| SSG / ISR (prerendered) | Complete | Best, and fastest |
| SSR (per request) | Complete | Excellent |
| CSR (client-only SPA) | Empty shell | Fragile: depends entirely on rendering, invisible to non-rendering bots |
The rule: content pages must arrive as server-rendered HTML. In the Next.js App Router this is the default: pages are Server Components, prerendered when possible. You opt into fragility by fetching content with useEffect in client components.
// ❌ Content exists only after a client-side fetch
"use client";
export default function Post() {
const [post, setPost] = useState(null);
useEffect(() => { fetch(`/api/post`).then(/* … */); }, []);
return post ? <Article post={post} /> : <Spinner />;
}
// ✅ Content is in the HTML every crawler fetches
export default async function Post({ params }) {
const post = await getPost((await params).slug);
return <Article post={post} />;
}The 2025-2026 rules Google wrote down#
Between December 2025 and March 2026 Google added several clarifications to its JavaScript docs. None changed how Googlebot behaves, but each one explains a class of bugs:
| Date | What Google documented | What to do |
|---|---|---|
| 2025-12-15 | When Google sees noindex, it "may skip rendering and JavaScript execution", so JS that removes or changes a noindex "may not work as expected" | Never ship noindex in the initial HTML of a page you want indexed |
| 2025-12-17 | Canonicalization happens before and after rendering. Don't use JS to change the canonical to a different value from the original HTML | Set the canonical in HTML. If you can't, leave it out of the HTML and inject one tag with JS |
| 2025-12-18 | Pages with a non-200 status "might" skip rendering | Don't rely on JS to fill in content on 404 or 5xx responses |
| 2026-03-24 | Google respects robots meta tags even when placed in the <body> | A stray noindex rendered by any component counts |
Two older rules still apply: the Web Rendering Service is stateless (cookies are not kept, and localStorage, sessionStorage and service workers are cleared between page loads), and it caches resources aggressively and may ignore your caching headers, so use content-hashed filenames (main.2bb85551.js) to avoid stale bundles.
The classic failure modes#
Links that aren't links#
Crawlers follow <a href>. They do not click divs, and they don't resolve #/fragment routes:
// ❌ undiscoverable
<div onClick={() => router.push("/pricing")}>Pricing</div>
// ✅ a real anchor (Next.js <Link> renders one)
<Link href="/pricing">Pricing</Link>Client-side-only metadata#
Titles, canonicals and robots tags set by client JS arrive late at best, and conflict with the HTML at worst (see the table above). Use the server-side Metadata API so they are in the initial HTML.
Soft 404s#
A SPA that renders "Not found" with HTTP 200 fills the index with junk URLs. Return a real 404: in Next.js, call notFound() from the page. A pure client-side SPA has two documented options: redirect with JS to a URL that returns a server 404, or add <meta name="robots" content="noindex"> to the error view.
Content behind interaction#
Tabs, accordions and "load more" content that is only fetched on click doesn't exist for crawlers. If it must be indexed, render it in the HTML (hidden-but-present is fine) or give it its own URL.
Blocked resources#
If robots.txt disallows the JS bundles or API routes your page needs to render, Google renders a broken page. Let crawlers fetch your assets.
Web components#
Google flattens shadow DOM and light DOM when rendering. Content that doesn't appear in the rendered HTML (for example, light DOM content with no <slot>) is not indexed.
How to verify what bots see#
curlthe page: is your content in the raw HTML? This is also what most AI crawlers get.
curl -s https://example.com/post | grep -c "your headline"- URL Inspection in Search Console → "Test live URL" → "View tested page" shows the rendered HTML, a screenshot and any JavaScript console errors: ground truth for Googlebot.
- Disable JavaScript in DevTools and reload: whatever survives is what non-rendering crawlers see.
- Crawl with JS rendering on and off (Screaming Frog and Sitebulb support both) and diff the discovered links and word counts. The delta is your JS dependency.
Next: International SEO, serving the right language and country variant to the right searcher.