# JavaScript SEO: how Google crawls, renders and indexes JavaScript

URL: https://serpel.app/blog/javascript-seo

Updated: 2026-10-10

JavaScript SEO is the work of making sure search engines can crawl, render and index content that your scripts create. Google renders JavaScript with a headless Chromium after it crawls a page, so client-side content can be indexed, just later than content that is already in the HTML. Other crawlers, including the main AI crawlers, may not run scripts at all, so the content, links and metadata that matter belong in the HTML you ship.

## Key takeaways

- Google queues every page that returns a 200 status for rendering and indexes the rendered HTML, but a page can wait in the render queue for longer than a few seconds.
- Crawlers only follow links that are `<a>` elements with an `href`, and they only see content that loads without scrolling, clicking or blocked scripts.
- Server-side rendering, static rendering or hydration give every crawler the same HTML. Google calls dynamic rendering a workaround, not a recommended solution.
- Compare the raw HTML with the rendered DOM and confirm the result with the URL Inspection tool in Search Console.

## How does Google crawl, render and index JavaScript?

Google describes [three phases for JavaScript web apps](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics): crawling, rendering and indexing. Googlebot fetches a URL after it checks `robots.txt`. Every page that returns a `200` status code is queued for rendering, and once Google’s resources allow, a headless Chromium renders the page and runs the JavaScript. Google then uses the rendered HTML to index the page and parses it again for links to crawl.

Older guides talk about “two waves of indexing”. Google’s current documentation describes the three phases above and a render queue instead. A page may stay in that queue for a few seconds, but it can take longer. Client-side content can therefore be indexed, just later than content that is already in the HTML.

Four rules follow from how that pipeline works:

- **Blocked files are not rendered.** If `robots.txt` blocks a script or the page, Google does not run that JavaScript.
- **noindex can end the process early.** When Google sees `noindex` in the HTML it may skip rendering, so a script that removes the tag later may never run.
- **Only real links are discovered.** Google can only find links that are `<a>` elements with an `href` attribute.
- **HTML metadata wins.** JavaScript can set the title, description and canonical, but HTML is preferred, and a canonical set by script must match the one in the HTML.

## Which rendering strategy is best for JavaScript SEO?

The rendering strategy decides what the first response contains. Google copes with all of them, but many other crawlers do not. The table follows the definitions in [web.dev’s guide to rendering on the web](https://web.dev/articles/rendering-on-the-web).

**Rendering strategies compared for crawlers**
| Strategy | HTML in the first response | Risk for crawlers | Good fit |
| --- | --- | --- | --- |
| Client-side rendering (CSR) | An empty shell and script tags | High: content exists only after rendering, and a crawler that does not render sees nothing | Logged-in apps and dashboards |
| Server-side rendering (SSR) | Complete HTML for every request | Low: the cost is server time and a slower first byte | Personalised or fast-changing pages |
| Static rendering (SSG) | Complete HTML built once at build time | Lowest: changing content needs a rebuild | Docs, blogs and marketing pages |
| Incremental static regeneration (ISR) | Prebuilt HTML that the server refreshes in the background, as [Next.js describes it](https://nextjs.org/docs/app/getting-started/caching) | Low: content can be briefly stale | Large catalogues of mostly stable pages |
| Dynamic rendering | Rendered HTML for bots, a client-side app for users | Medium: two code paths to keep in sync | Avoid, see below |

Google says dynamic rendering [was a workaround and not a long-term solution](https://developers.google.com/search/docs/crawling-indexing/javascript/dynamic-rendering) and recommends server-side rendering, static rendering or hydration. Hydration adds client scripts to server-rendered HTML. It keeps the HTML complete for crawlers, but web.dev notes it can have a significant negative impact on Total Blocking Time and Interaction to Next Paint. For a framework-specific walkthrough, see our guide to [Next.js SEO](https://serpel.app/blog/nextjs-seo).

The choice matters beyond Google. [Vercel’s analysis from December 2024](https://vercel.com/blog/the-rise-of-the-ai-crawler) found that none of the major AI crawlers it examined rendered JavaScript, among them OpenAI’s, Anthropic’s and Perplexity’s, while Googlebot and Applebot did. Crawler behaviour changes, so treat that as a dated snapshot and check your own server logs. The safe rule is to ship the content, links and metadata that matter in the initial HTML.

## What are the most common JavaScript SEO problems?

Most failures come from a handful of patterns. Each row names the symptom and the fix.

**Common JavaScript SEO failures**
| Problem | What goes wrong | Fix |
| --- | --- | --- |
| Empty HTML shell | The delivered HTML holds a root `<div>` and scripts. Text and links appear only after rendering. | Server-render or prerender indexable routes and check them with `curl`. |
| Links without `href` | Buttons, `onclick` handlers and framework attributes navigate in a browser. Google [can’t reliably extract URLs](https://developers.google.com/search/docs/crawling-indexing/links-crawlable) from `<a>` elements without `href`. | Use `<a href="/path">` for every internal link. |
| Fragment URLs | Routes such as `#/products` can’t be resolved reliably by Googlebot. | Use the History API with real paths. |
| Soft 404s in single-page apps | Unknown routes render a “not found” view but the server answers `200`. | Redirect to a URL that returns 404 or add `noindex` to error views. See [soft 404](https://serpel.app/blog/soft-404). |
| Lazy-loaded content | Google does not scroll or click, so content that loads only on interaction never appears. | Load content when it enters the viewport and give infinite scroll [paginated URLs](https://developers.google.com/search/docs/crawling-indexing/javascript/lazy-loading). |
| Blocked or oversized files | `robots.txt` blocks scripts, or a bundle is large. Googlebot fetches each resource separately and [reads the first 2 MB of it](https://developers.google.com/search/docs/crawling-indexing/googlebot). | Allow script and CSS paths. Split large bundles. |
| Metadata changed by script | Title, canonical or robots differ between the HTML and the rendered DOM. | Set them in the HTML. Never add `noindex` to the HTML if you want the page indexed. |
| Structured data from scripts | Google can read JSON-LD that JavaScript generates, but crawlers that do not render never see it. | Put JSON-LD in the HTML where you can and test the rendered result with the Rich Results Test or by pasting the rendered HTML into the free [schema validator](https://serpel.app/tools/schema-validator). |
| Stale cached scripts | Google’s renderer may ignore caching headers and run an old script. | Put a content hash in file names so a new release gets a new URL. |

## How do you test what Google actually sees?

Test in three layers: the HTML your server sends, the DOM after rendering, and Google’s own view. A mismatch between the first two is the most common finding.

1. **Look at the delivered HTML** Fetch the page without a browser and count the headings and links that are already there. If the counts are zero, everything depends on rendering.

```bash
curl -s https://example.com/pricing -o raw.html
grep -o "<h1" raw.html | wc -l
grep -o "<a [^>]*href=" raw.html | wc -l
```

2. **Render it in a real browser** Save this small [Playwright](https://playwright.dev/docs/api/class-page) script as `renderPage.mjs`. It prints the full HTML after the scripts have run, including the doctype.

```javascript
import { chromium } from 'playwright'

const browser = await chromium.launch()
const page = await browser.newPage()
await page.goto(process.argv[2], { waitUntil: 'networkidle' })
process.stdout.write(await page.content())
await browser.close()
```

3. **Compare the two** Run the same counts on the rendered file. Large differences in headings, links or words show what a non-rendering crawler misses.

```bash
node renderPage.mjs https://example.com/pricing > rendered.html
grep -o "<h1" rendered.html | wc -l
grep -o "<a [^>]*href=" rendered.html | wc -l
```

4. **Ask Google** The URL Inspection tool shows loaded resources, JavaScript console output and exceptions, and the rendered DOM, as described in [Google’s debugging guide](https://developers.google.com/search/docs/crawling-indexing/javascript/fix-search-javascript). Run a live test and open “View tested page” to see how Google [renders the URL](https://support.google.com/webmasters/answer/7440203). The Rich Results Test can also confirm that the rendered HTML contains your content.

Read the comparison like an auditor. These differences matter most:

- **Headings and text:** a missing H1 or a large gap in word count means the main content depends on rendering.
- **Links:** internal links that appear only in the rendered file will not be followed by a crawler that does not render.
- **Title, description and canonical:** values that change after rendering tell different crawlers different stories.
- **Robots directives:** a `noindex` or `nofollow` that appears or disappears after rendering is a bug that belongs in the HTML.

## How does Serpel’s crawler render JavaScript pages?

A JavaScript SEO audit with [Serpel’s site audit](https://serpel.app/features/site-audit) starts from the HTML the server delivers, then decides per page whether to render it. The project setting has three modes: “Automatic for empty pages” (the default), “Always render” and “Never render”. In automatic mode a page counts as a likely JavaScript shell when its delivered HTML has fewer than 50 words and shows a sign of a framework: a root container such as `#__next`, `#__nuxt`, `#root` or `#app`, Angular’s `<app-root>`, a `<noscript>` message about JavaScript, or several scripts with no links, no H1 and almost no text.

Pages that qualify are rendered in headless Chromium, using a mobile or desktop viewport that matches the crawl device. The renderer waits for the network to go idle and falls back to the `load` event if that times out. A crawl renders at most a fixed number of pages and reports when it reaches the limit. After rendering, the title, headings, links and the other checks run on the rendered page, and the crawl compares it with the raw HTML.

- **Content only via JavaScript:** the rendered page has at least 50 words and the raw HTML has less than half as many.
- **Links only via JavaScript:** at least 3 internal links exist only after rendering, and the raw HTML holds fewer than half of the rendered links.
- **Metadata changed by JavaScript:** title, description or H1 differ, or the canonical or robots directive differs between the two versions.
- **Render problems:** the page could not be rendered, or the browser console reported JavaScript errors.
- **Rendering recommended:** the page looks like an empty shell but was not rendered, because rendering is off, unavailable, over the limit or failed. Serpel reports this instead of false “missing title” or “missing H1” errors.

```bash
serpel projects update <project-id> --render-mode always
serpel crawl start --project <project-id> --wait
serpel audit issues --project <project-id> --category rendering
```

> **A check, not a copy of Googlebot:** Serpel uses its own Chromium, so it can’t reproduce Google’s render queue or its resource limits. Use it to find pages that depend on JavaScript, then confirm the important ones with URL Inspection.

## What should you do first?

1. Fetch the raw HTML of your main templates and check that the H1, text and navigation links are present.
2. Replace click handlers that navigate with real `<a href>` links.
3. Return real status codes: `404` or `410` for missing content, `301` for moved content.
4. Server-render or prerender everything you want indexed.
5. Make sure `robots.txt` does not block your scripts and stylesheets.
6. Crawl the site with rendering on and review the pages where raw and rendered HTML differ.

## Frequently asked questions

### Does Google crawl and render JavaScript?

Yes. Googlebot queues pages that return a 200 status for rendering, a headless Chromium runs the JavaScript, and Google indexes the rendered HTML. Rendering can wait in a queue, and blocked resources or a `noindex` tag can stop it.

### Is JavaScript bad for SEO?

Not by itself. Problems start when content, links or metadata exist only after rendering that a crawler does not perform or that fails. Server-render the essentials and use JavaScript for interactivity on top.

### Do AI crawlers run JavaScript?

In Vercel’s December 2024 analysis, none of the major AI crawlers it examined rendered JavaScript. Behaviour can change, so keep key content in the HTML. Our [AI crawlers guide](https://serpel.app/blog/ai-crawlers) covers which bots exist and how to control them.

### Is dynamic rendering still recommended?

No. Google describes dynamic rendering as a workaround that adds complexity and resource costs, and recommends server-side rendering, static rendering or hydration instead.

### How do I check whether Google can see my JavaScript content?

Open the URL Inspection tool in Search Console, run a live test and read the rendered DOM and console messages under “View tested page”. Compare it with the HTML your server sends, for example with `curl`.

## Sources

- [Google Search Central: Understand JavaScript SEO basics](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics), accessed 2026-10-10
- [Google Search Central: Fix Search-related JavaScript problems](https://developers.google.com/search/docs/crawling-indexing/javascript/fix-search-javascript), accessed 2026-10-10
- [Google Search Central: Make your links crawlable](https://developers.google.com/search/docs/crawling-indexing/links-crawlable), accessed 2026-10-10
- [Google Search Central: Fix lazy-loaded content](https://developers.google.com/search/docs/crawling-indexing/javascript/lazy-loading), accessed 2026-10-10
- [Google Search Central: Dynamic rendering as a workaround](https://developers.google.com/search/docs/crawling-indexing/javascript/dynamic-rendering), accessed 2026-10-10
- [Google Search Central: Googlebot](https://developers.google.com/search/docs/crawling-indexing/googlebot), accessed 2026-10-10
- [Search Console Help: Page indexing report](https://support.google.com/webmasters/answer/7440203), accessed 2026-10-10
- [web.dev: Rendering on the web](https://web.dev/articles/rendering-on-the-web), accessed 2026-10-10
- [Vercel: The rise of the AI crawler (17 December 2024)](https://vercel.com/blog/the-rise-of-the-ai-crawler), accessed 2026-10-10
- [Next.js documentation: Caching and Incremental Static Regeneration](https://nextjs.org/docs/app/getting-started/caching), accessed 2026-10-10
- [Playwright documentation: Page](https://playwright.dev/docs/api/class-page), accessed 2026-10-10