Google does execute JavaScript, but not in one step and not with any timing guarantee. Understanding the two-pass process — crawl, then render — explains nearly every JavaScript SEO problem you will encounter, from missing content to metadata that never appears in search results.
How Googlebot actually processes a JavaScript page
- Crawl. Googlebot requests the URL and receives the initial HTML — for a client-rendered app, often an empty
<div id="root">. - Queue for rendering. The URL enters a render queue. This can be minutes or considerably longer, depending on resources and site priority.
- Render. A headless Chromium instance executes the JavaScript, fetches APIs, and produces the rendered DOM.
- Index. Only now is the JS-generated content available for indexing, along with any links discovered during rendering.
The failure modes, in order of frequency
1. Content that depends on a client-side API call
If the product description arrives from /api/products/123 after mount, an API timeout, a CORS failure or a 401 during rendering yields an indexed page with no content. Google will not retry immediately.
2. Links that are not links
<!-- Not crawlable: no href, discovered only if rendering succeeds -->
<div onclick="navigate('/services/technical-seo-audits')">Audits</div>
<!-- Crawlable -->
<a href="/services/technical-seo-audits">Technical SEO audits</a>
Router components in React and Vue generally render a real anchor with an href — but only if you use the framework's link component. Hand-rolled click handlers, buttons that call router.push, and infinite scroll without paginated URLs all create sections Google cannot reach.
3. Blocked resources
If robots.txt disallows the directory holding your JS bundle, rendering fails and the page is evaluated as a shell. Check the "View crawled page → More info → Page resources" list in URL Inspection for any resource Google could not load.
4. Metadata written after hydration
Client-side helpers mutate document.head after the app boots. Googlebot generally picks this up post-render. Facebook, LinkedIn, Slack and WhatsApp do not — they read the raw HTML response. That is why a JS-only og:image produces a blank link preview even when the page ranks fine.
5. Soft 404s from client-side routing
An SPA rendering a "Not found" component still returns HTTP 200. Handle unknown routes on the server so the correct status code is emitted.
Choosing a rendering strategy
| Strategy | Indexing reliability | Best for |
|---|---|---|
| Static generation (SSG) | Highest | Marketing sites, docs, blogs — content that changes on a deploy cadence |
| Server-side rendering (SSR) | High | Catalogues, listings, personalised-but-public pages |
| Incremental / cached SSR | High | Large catalogues where full rebuilds are impractical |
| Client-side rendering (CSR) | Variable | Authenticated dashboards and app screens that should not be indexed anyway |
| Dynamic rendering | Workaround | Legacy applications awaiting a rewrite |
The useful rule: public, indexable content should exist in the HTML response. Everything behind a login can be client-rendered freely.
Debugging workflow
- View source, not DevTools.
curl https://example.com/pageshows the initial HTML. DevTools shows the post-hydration DOM and hides the problem. - URL Inspection → Test live URL → View crawled page. Search the rendered HTML for a unique sentence from your main content.
- Check the resources tab for blocked or failed assets.
- Check the JavaScript console output in the live test for uncaught errors during render.
- Crawl with JS rendering enabled and disabled and compare URL counts. A large gap tells you how much of the site depends on rendering.
Performance is part of the problem
Heavy JavaScript hurts twice: it delays rendering for Google and damages Interaction to Next Paint for users. Practical measures that help both:
- Route-level code splitting so a landing page does not ship the dashboard bundle.
- Server-render above-the-fold content; hydrate below-the-fold components lazily.
- Keep third-party scripts out of the critical path and load them after interaction where possible.
- Set a bundle-size budget in CI so regressions are caught before deployment.
Framework-specific notes
- React: Next.js App Router with Server Components keeps content and metadata in the HTML response. A plain Vite SPA does not — for content sites, that difference matters.
- Vue: Nuxt in universal mode is the equivalent choice.
- Headless WordPress / Shopify: render at the edge or statically; do not let the storefront depend on client-side API calls for product copy.
- Laravel with Inertia or Livewire: both server-render by default, which is why Laravel applications rarely have this class of problem — see the Laravel SEO guide.
A short pre-launch checklist
- Main content appears in
view-source, not just in DevTools. - Every navigational element is an
<a href>. - Title, description and canonical are in the HTML response.
- Unknown routes return HTTP 404 from the server.
- JS and CSS directories are crawlable in robots.txt.
- Paginated and filtered states have real URLs where they should be indexable.
- Live test in URL Inspection shows the complete rendered content.