# Does Google crawl JavaScript? How rendering works in 2026 and what still breaks

> Yes, Google crawls and renders JavaScript in headless Chromium after a render queue. What still fails, from blocked files to 2MB limits, and how to check.

Source: https://seofix.ai/blog/does-google-crawl-javascript  
Published: 2026-11-04  
Updated: 2026-11-02  
Author: SEOFix Team  
Category: Technical SEO  
Tags: Technical SEO, Guides  
Length: 2,387 words, 11 min read

Yes. Google crawls JavaScript sites and renders them: every page that answers `200` goes into a render queue, and a headless Chromium runs the JavaScript before Google indexes the result ([Google](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics)). For most pages the wait is seconds. Pages still fail, though, and the reasons are specific: JavaScript files blocked in robots.txt, a `noindex` in the initial HTML, routes behind `#` fragments, content that waits for a click or a scroll, files cut off at 2MB, and error pages that answer `200`. This article goes through each one and shows how to check it on your own pages.

Other crawlers are a different story. Most AI crawlers fetch the HTML and stop, which we covered in [Can AI crawlers see your site?](https://seofix.ai/blog/do-ai-crawlers-render-javascript). Here we stick to Googlebot.

## How Google crawls JavaScript: crawl, render, index

Google describes three phases: crawling, rendering and indexing ([JavaScript SEO basics](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics)).

1. **Crawl.** Googlebot reads robots.txt, fetches the URL and parses links from the `href` attributes in the HTML it got back. If robots.txt disallows the URL, Google skips it, and ["Google Search won't render JavaScript from blocked files or on blocked pages"](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics).
2. **Render.** ["Googlebot queues all pages with a 200 HTTP status code for rendering, unless a robots meta tag or header tells Google not to index the page."](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics) When resources allow, a headless Chromium runs the page. Google calls it an evergreen version of Chromium, so modern JavaScript syntax is fine.
3. **Index.** Google parses the rendered HTML for links again and uses it to index the page.

Two details in that sequence cause most real-world problems. The render step only happens for `200` pages: since December 2025 the docs say that for non-200 responses ["rendering might be skipped"](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics) ([changelog](https://developers.google.com/search/updates)). And a `noindex` in the HTML can stop rendering before your JavaScript ever runs.

### How long the render queue takes

Google's own wording is that a page ["may stay on this queue for a few seconds, but it can take longer than that"](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics). The best public measurement is from Vercel and MERJ, who matched over 37,000 Googlebot crawls on nextjs.org with the render that followed, in April 2024 ([Vercel](https://vercel.com/blog/how-google-handles-javascript-throughout-the-indexing-process)):

| Percentile | Time from crawl to render |
| --- | --- |
| 25th | within 4 seconds |
| 50th | 10 seconds |
| 75th | 26 seconds |
| 90th | about 3 hours |
| 99th | about 18 hours |

The same study found that 100% of the indexable HTML pages it tracked were fully rendered, including pages with complex JavaScript and streamed React Server Components. One site, one month, two years ago, so treat the numbers as an order of magnitude. They do settle the old "Google renders weeks later" worry for a well-linked site. A page that still isn't indexed after a few weeks usually has one of the problems below.

### The 2MB limit applies to JavaScript files too

In March 2026 Google explained that ["Googlebot currently fetches up to 2MB for any individual URL (excluding PDFs)"](https://developers.google.com/search/blog/2026/03/crawler-blog-post), headers included. Anything past the cut-off is ignored: not fetched, rendered or indexed. The renderer fetches each script, stylesheet and XHR response as a separate URL with its own counter, and ["for each requested resource, the 2MB limit also applies"](https://developers.google.com/search/blog/2026/03/crawler-blog-post).

For a client-rendered app this matters in two places. An HTML document stuffed with a large inline JSON state blob can push the content past 2MB. And a single unsplit bundle over 2MB is truncated, which on a single-page app usually means the app doesn't boot at all for Google. Check your largest files:

```bash
# size of the HTML Googlebot would get
curl -s -o /dev/null -w '%{size_download} bytes\n' https://example.com/some-page

# sizes of every script the page references (same-origin, absolute or root-relative)
curl -s https://example.com/some-page \
  | grep -o '<script[^>]*src="[^"]*"' | sed 's/.*src="//; s/"$//' \
  | while read -r src; do
      case "$src" in http*) u="$src";; *) u="https://example.com$src";; esac
      printf '%10s  %s\n' "$(curl -s -o /dev/null -w '%{size_download}' --compressed "$u")" "$u"
    done | sort -n
```

`size_download` with `--compressed` is the transferred size. Google's post talks about bytes fetched, and doesn't say whether that means compressed or uncompressed, so leave yourself headroom and split any bundle that gets close.

## What still breaks Google's JavaScript rendering

Google's [troubleshooting guide](https://developers.google.com/search/docs/crawling-indexing/javascript/fix-search-javascript) and the JavaScript basics page list the failure modes. These are the ones we see most, roughly in order of how much damage they do.

### 1. JavaScript or API endpoints blocked in robots.txt

A `Disallow: /static/` or `Disallow: /api/` written years ago to "save crawl budget" means the renderer can't load the bundle or the data, and Google indexes an empty shell. The Crawl Stats report warns about exactly this: if Google needs CSS or JavaScript to understand the content, ["be sure you do not block them from Googlebot"](https://support.google.com/webmasters/answer/9679690).

Check with the live test in URL Inspection (below): under **View tested page → More info → Page resources**, blocked files are listed with the reason.

### 2. `noindex` in the initial HTML, removed by JavaScript

A common pattern in SPAs: the HTML template ships `<meta name="robots" content="noindex">` as a safe default, and the app removes it once content loads. Google warns that ["when Google encounters the noindex tag, it may skip rendering and JavaScript execution"](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics), and Vercel's test found that pages with `noindex` in the initial HTML were not rendered at all ([Vercel](https://vercel.com/blog/how-google-handles-javascript-throughout-the-indexing-process)). The script that removes the tag never runs for Google, so the page stays out.

The reverse works: JavaScript can add `noindex` to a page whose HTML allows indexing, and Google applies it. If a page should be indexed, keep `noindex` out of the server HTML. We wrote up the related traffic puzzle in [Noindex page still getting Google traffic](https://seofix.ai/blog/noindex-page-still-getting-traffic).

### 3. Routes behind `#` fragments

Google can only discover links that are `<a>` elements with an `href` ([Google](https://developers.google.com/search/docs/crawling-indexing/links-crawlable)), and it won't treat `#/products` and `#/services` as separate pages. The troubleshooting guide is direct: the AJAX crawling scheme ["has been deprecated since 2015, so you can't rely on URL fragments to work with Googlebot"](https://developers.google.com/search/docs/crawling-indexing/javascript/fix-search-javascript). Use the History API (`pushState`) and real paths. Angular's router uses them by default, and hash URLs are opt-in through `withHashLocation()` ([Angular](https://angular.dev/guide/routing/router-reference)). Vue Router makes you pick `createWebHistory` or `createWebHashHistory`, and its docs say hash mode ["does however have a bad impact in SEO"](https://router.vuejs.org/guide/essentials/history-mode.html). In React Router, avoid `HashRouter`.

Links built from `<span onclick>` or `<a onclick="goto(...)">` without an `href` aren't followed either. Google's [link guidance](https://developers.google.com/search/docs/crawling-indexing/links-crawlable) lists `<a routerLink="products/category">` as not crawlable on its own. Angular's `RouterLink` applies an `href` to `<a>` elements when it renders ([Angular](https://angular.dev/api/router/RouterLink)), so check the rendered HTML rather than the template.

### 4. Content that waits for interaction

Google doesn't click, type or scroll. Its lazy-loading guide says loading methods must not rely on user actions ["as Google Search does not interact with your page"](https://developers.google.com/search/docs/crawling-indexing/javascript/lazy-loading). Product descriptions behind a "Read more" click that triggers a fetch, reviews loaded on scroll without an `IntersectionObserver`, tab panels that fetch on click: none of that content exists for Google. Infinite scroll needs real paginated URLs like `?page=12` that each load the same chunk every time.

The fix is to put the content in the DOM when the page loads and toggle its visibility with CSS, instead of fetching it on click.

### 5. State that only exists in a browser session

The renderer ["does not retain state across page loads"](https://developers.google.com/search/docs/crawling-indexing/javascript/fix-search-javascript): localStorage, sessionStorage and cookies are cleared. A page that reads a city or a currency from localStorage and renders nothing when it's missing renders nothing for Google. Googlebot also declines permission prompts (camera, location) and doesn't support WebSockets or WebRTC, so content streamed over a WebSocket needs an HTTP fallback.

### 6. Error pages that answer `200` (soft 404s)

A client-side router can't change the HTTP status. When `/products/discontinued-thing` loads the app, fetches the product, gets nothing and shows "Not found", the server already answered `200`. Google may index the error page or report it as a soft 404. Google's two documented fixes for client-rendered apps are a JavaScript redirect to a URL that does return `404`, or adding `noindex` with JavaScript on error views ([Google](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics)):

```javascript
fetch(`/api/products/${productId}`)
  .then((res) => res.json())
  .then((product) => {
    if (!product.exists) {
      // the server answers 404 for this path
      window.location.href = '/not-found'
    }
  })
```

With server rendering you can send a real `404`. That has its own trap in frameworks that stream: Next.js documents that when `notFound()` runs after a `<Suspense>` boundary has started streaming, the response ["keeps its 200 status"](https://nextjs.org/docs/app/api-reference/functions/not-found) and Next.js adds a `noindex` tag instead. Do the lookup before anything streams if you want the status code.

### 7. A canonical that JavaScript changes

Since December 2025 Google says canonicalization ["happens before and after rendering"](https://developers.google.com/search/updates). You may set the canonical with JavaScript, but you ["shouldn't use JavaScript to change the canonical URL to something else than the URL you specified as the canonical URL in the original HTML"](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics). Set it in the server HTML, or leave it out of the HTML and set it only with JavaScript. Never both with different values.

### 8. Stale cached bundles and runtime errors

The renderer ["may ignore caching headers"](https://developers.google.com/search/docs/crawling-indexing/javascript/fix-search-javascript), so after a deploy it can run an old `main.js` against new HTML. Fingerprinted filenames (`main.2bb85551.js`), which every modern bundler produces by default, avoid that. Runtime errors do as much damage: a `window.foo` that is undefined in Google's environment throws and leaves the page half-rendered. The live test's JavaScript console output shows these.

### What about timeouts?

Google doesn't publish a render timeout, and we won't quote the numbers that circulate on forums. Google does say that the renderer skips resources it judges non-essential, such as analytics and error-reporting calls ([Google](https://developers.google.com/search/docs/crawling-indexing/javascript/fix-search-javascript)). In practice, content that depends on a chain of three sequential API calls is more fragile than content that's in the first response. If you can't tell whether a slow page renders, the live test will show you what Google got.

## How to check whether Google renders your page

### URL Inspection live test

In Search Console, inspect the URL and choose **Test live URL**. Under **View tested page** you get the HTML, a screenshot, the JavaScript console output and the list of page resources ([Google](https://support.google.com/webmasters/answer/9012289)). Google's guide to web components tells you to look at the rendered HTML there to confirm content is visible ([Google](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics)).

What to look for:

- **HTML tab:** search (Ctrl+F) for a sentence from the main content and for a link that should be there. If it's missing, Google didn't get it.
- **More info → Page resources:** anything marked blocked or "Other error". A blocked `.js` file from your own domain is the first thing to fix.
- **More info → JavaScript console messages:** uncaught exceptions.
- **Screenshot:** a spinner or an empty layout is a bad sign, but the screenshot only shows the top of the page. Trust the HTML tab.

The live test has a per-property daily limit and doesn't predict canonical selection or quality-based exclusion ([Google](https://support.google.com/webmasters/answer/9012289)). It answers one question: can Google fetch and render this URL now.

### Rich Results Test, for pages you don't own

The [Rich Results Test](https://support.google.com/webmasters/answer/7445569) renders any public URL with the same Google-InspectionTool crawler and shows the rendered HTML, smartphone by default. It works for a competitor's page or a staging URL exposed through a tunnel, and it doesn't need Search Console access.

### Crawl Stats for the whole site

**Settings → Crawl stats** breaks Google's requests down by file type, including JavaScript, and by Googlebot type, where **Page resource load** is the renderer fetching scripts and styles ([Google](https://support.google.com/webmasters/answer/9679690)). A drop in JavaScript fetches, or a rise in 4xx responses on `/static/`, after a deploy or a CDN change is worth chasing.

For a deeper walk-through of the testing tools and a curl-vs-headless-Chrome comparison, see How to test JavaScript crawlability.

## Should you still server-render?

Rendering works for most pages, and Google still says ["server-side or pre-rendering is still a great idea because it makes your website faster for users and crawlers, and not all bots can run JavaScript"](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics). Every problem in the list above either disappears or gets easier with server HTML: links are in the first response, status codes are real, `noindex` and canonicals are set once, and nothing depends on a 2MB bundle booting. Framework-specific guides for this cluster: React single-page apps, Angular and Nuxt. If you're tempted to serve bots a prerendered copy instead, read dynamic rendering first; Google calls that approach a workaround.

## Checking every template at once

URL Inspection tests one URL at a time. Sites break by template, so test one URL from each page type: home, category, product, article, search.

SEOFix runs its on-page checks on the raw HTML of every page it crawls, so a template whose title, H1 or links only appear after JavaScript shows up as missing fields across that template. On full audits where the render sample is enabled, it also loads up to 20 of the shallowest pages in headless Chromium and flags `JS_DEPENDENT_CONTENT` when the title, canonical, H1 count, word count or link count changes after rendering, and `JS_NOINDEX_MISMATCH` when JavaScript adds or removes `noindex`. The sample is optional and doesn't cover content that needs a login, a click or more than about two seconds to appear, so a clean result doesn't prove a page renders. Details are in [JavaScript rendering](https://seofix.ai/help/javascript-rendering) and the [AI visibility and rendering checks](https://seofix.ai/help/checks-ai-and-rendering) reference. Once Search Console is connected, [the Google tab](https://seofix.ai/help/google-indexing-tab) shows which of those pages Google actually indexed.

## FAQ

### Does Googlebot execute JavaScript on every page?

On every page that answers `200` and doesn't carry `noindex`, Google says it queues the page for rendering, whether or not it uses JavaScript ([Google](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics)). Non-200 pages may not be rendered.

### Does Google index content loaded with fetch or XHR after page load?

Yes, if the request succeeds in Google's renderer: the API isn't blocked by robots.txt, doesn't need cookies or local storage, and doesn't wait for a click or scroll. The renderer processes XHR requests as part of rendering ([Google](https://developers.google.com/search/blog/2026/03/crawler-blog-post)).

### Does Google crawl links added by JavaScript?

Yes, as long as they're `<a href>` elements in the rendered HTML. Google says injecting links into the DOM with JavaScript is fine if they follow its [crawlable link rules](https://developers.google.com/search/docs/crawling-indexing/links-crawlable).

### Is JavaScript bad for SEO?

Google handles it. The risk is that your content depends on more things going right: the bundle loads, the API answers, nothing throws, nothing waits for a click. Server-rendered HTML removes most of those dependencies and also works for crawlers that don't run JavaScript.

---

Want to know which of your templates only work after JavaScript runs? [Run a free audit](https://seofix.ai/): SEOFix reads the HTML your server returns on every page and groups what's missing by template.
