Google can index JavaScript sites, but it does so in two passes. First it crawls the raw HTML. Later, a headless Chromium renders the page, runs your scripts and hands the final DOM to indexing. Anything that goes wrong in between (blocked scripts, failed API calls, links that are not real links, wrong status codes) leaves Google with less than what your users see. The reliable fix is to put the important things in the server response.
The three phases
- Crawling. Googlebot requests the URL after checking robots.txt. It reads the response and extracts links from
hrefattributes in the HTML it received. - Rendering. Pages that return a 200 status are queued for rendering. When resources allow, the Web Rendering Service (WRS) loads the page in an evergreen Chromium, executes JavaScript and produces rendered HTML. This can happen within seconds, or it can take considerably longer.
- Indexing. Google processes the rendered HTML: content, links, canonical, robots meta, structured data. Links found at this stage go back into the crawl queue.
Three details in that flow explain most JavaScript SEO problems:
- Only 200 responses are sure to be rendered. For other status codes, rendering may be skipped.
- A noindex in the raw HTML can stop the process. Google's documentation says that when it encounters noindex, it may skip rendering and JavaScript execution. A script that removes the tag afterwards may never run.
- Rendering is a separate queue. On a server-rendered site, a new link is discovered at crawl time. On a client-rendered site, it is discovered only after rendering, so each level of depth can add delay.
What commonly breaks
Navigation that is not made of links
Google follows <a> elements with an href that resolves to a URL. It does not click.
<!-- Crawlable -->
<a href="/products/boots">Boots</a>
<a href="/products/boots" onclick="track()">Boots</a>
<!-- Not crawlable -->
<span onclick="go('/products/boots')">Boots</span>
<a onclick="go('/products/boots')">Boots</a>
<a href="javascript:void(0)">Boots</a>
<button data-url="/products/boots">Boots</button>
Framework router components (<Link> in Next.js, <NuxtLink>, <RouterLink>) output real anchors, which is fine. Custom click handlers on divs are not. You can list the links a crawler can actually extract from a page with the Page Link Extractor.
Fragment routing
URLs such as example.com/#/products/boots rely on the fragment, which Google generally ignores. Every route looks like the same URL. Use the History API so each view has a real path:
// Instead of location.hash = "/products/boots"
history.pushState({}, "", "/products/boots");
And make sure the server answers those paths directly. A deep link that only works after loading the homepage first is not a URL a crawler can use.
Blocked resources
If robots.txt disallows your script bundles, your API, or the CDN host they are served from, the WRS cannot fetch them, and the page renders without its content. Remember that robots.txt is per host: the rules that matter for api.example.com are in the robots.txt on that host. Check with a URL Inspection live test, under View tested page, then More info, then Page resources.
Status codes in single page apps
A typical SPA serves the same index.html with status 200 for every path, including paths that do not exist. The app then shows a "Not found" view. To Google that is a soft 404 at best, and an indexable empty page at worst. Google documents two fixes when a real server-side 404 is not possible:
// Option 1: redirect to a URL where the server returns a real 404
if (!product) window.location.href = "/not-found";
// Option 2: add noindex to the error view
if (!product) {
const meta = document.createElement("meta");
meta.name = "robots";
meta.content = "noindex";
document.head.appendChild(meta);
}
The better fix is server-side: have the server check whether the resource exists and send a 404 status with the error page. More in soft 404 errors.
Metadata that starts wrong and gets "fixed" on the client
If the HTML shell ships a generic title, a canonical pointing at the homepage or a noindex, and JavaScript is meant to correct them per route, you are depending on rendering for your most sensitive signals. Conflicting canonicals between raw and rendered HTML are a classic cause of odd canonical choices. The noindex case can prevent rendering altogether. Ship correct values in the initial HTML, or ship none and add them once.
Content behind interaction
Tabs and accordions are fine when their content is in the DOM and merely hidden with CSS. Content fetched only when a user clicks "Load more" or opens a tab is never loaded by Googlebot. The same goes for infinite scroll that depends on scroll events: provide paginated URLs with real links as a fallback, and trigger lazy loading with IntersectionObserver, which the renderer supports, or with native loading="lazy" for images.
State, storage and permissions
The WRS loads each URL in a fresh session. Cookies, local storage and session storage do not persist between page loads, service workers are not kept, and permission prompts (location, notifications, camera) are declined. A page that shows content only after a consent choice or a location grant shows Google nothing.
Stale cached scripts
The WRS caches aggressively and may ignore caching headers. If you deploy a new main.js under the same filename, Google may render with the old one. Use content hashes in filenames (main.2bb85551.js), which most bundlers do by default.
Errors and timeouts
An uncaught exception early in your bundle, an API that answers slowly or rejects requests from Google's IP ranges, or a feature your code assumes without checking can each leave the page blank. There is no documented fixed timeout, so do not design around one. Make the main content render without waiting for slow secondary calls.
How to see what Google sees
URL Inspection, live test. In Search Console, inspect the URL, click Test live URL, then View tested page. You get the rendered HTML, a screenshot, the list of page resources that could not be loaded, and JavaScript console messages. Search the rendered HTML for a sentence from your main content and for your internal links.
Rich Results Test. Works for any public URL, including sites you have not verified, and also shows rendered HTML.
Compare raw and rendered. Fetch the raw HTML and look for your content:
curl -s -A "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" \
https://www.example.com/products/boots | grep -i "waterproof leather"
If the phrase is missing from the raw HTML and present in the rendered HTML, the page depends on rendering. That is not an error by itself, but it tells you where your risk is. The Meta Tags Analyzer shows the title, robots and canonical values present in the raw response, which is the quickest way to catch a wrong default in the shell.
Check the index, not just the test. A live test shows what Google can render now. To see what it actually indexed, use URL Inspection without the live test and view the crawled page, or search for an exact sentence in quotes together with site:yourdomain.com.
Choosing a rendering strategy
| Strategy | What the crawler gets in the first response | SEO risk | Notes |
|---|---|---|---|
| Static generation (SSG) | Full HTML | Lowest | Best for content that changes on publish, not per request |
| Server-side rendering (SSR) | Full HTML | Low | Needs server capacity. Watch response times |
| SSR or SSG with hydration | Full HTML, then interactive | Low | The default in Next.js, Nuxt, SvelteKit, Astro and Remix |
| Client-side rendering (CSR) | Empty shell plus scripts | Medium to high | Depends fully on the render step, invisible to non-rendering bots |
| Dynamic rendering | Prerendered HTML for bots only | Medium | Google calls it a workaround, not a long-term solution |
If you are starting a project where organic search matters, choose a framework mode that sends HTML. If you have an existing CSR app, move the pages that need to rank (product, category, article, landing pages) to SSR or prerendering first. Dashboards and logged-in areas can stay client-rendered since they should not be indexed anyway.
If you use dynamic rendering or a prerender service as a stopgap, serve bots the same content users get. Different content for crawlers is cloaking.
Does JavaScript slow down indexing?
It can. Google has narrowed the gap between crawling and rendering over the years, and for many pages rendering follows quickly. The remaining delay comes from the structure described above: links inside client-rendered content are found one render later than links in HTML, and heavy pages cost more to process, which matters on large sites where crawl budget is already tight. If a new client-rendered site has many pages stuck in discovered, currently not indexed, check how many of its internal links exist in the raw HTML. An XML sitemap removes the dependency on rendered links for discovery.
Checklist
- Every indexable view has its own URL path, reachable directly, returning 200.
- Non-existent paths return a real 404.
- Navigation uses
<a href>. - Title, meta description, canonical and robots are correct in the initial HTML.
- Main content is in the initial HTML, or appears without interaction after load.
- Scripts, styles and API endpoints needed for rendering are not blocked in robots.txt on any host.
- Script filenames are fingerprinted.
- Structured data is present in the rendered HTML (check with the Rich Results Test).
- All URLs are in the XML sitemap.
- URL Inspection live test shows your content, your links and no blocked critical resources.
Pass those ten and JavaScript stops being an indexing topic. At that point it is only a performance topic.
Frequently asked questions
- Can Google index content that is loaded with JavaScript?
- Yes. Googlebot renders pages with a current version of Chromium and indexes the resulting HTML. The content must appear without user interaction, the scripts and API calls it depends on must not be blocked by robots.txt, and rendering happens in a separate step that can lag behind the initial crawl.
- Is server-side rendering still better for SEO?
- It is the safer choice. With server-side rendering or static generation, the content, links and meta tags are in the first HTML response, so nothing depends on the rendering step. It also helps users and the many crawlers that do not run JavaScript at all.
- Do Bing and AI crawlers render JavaScript?
- Bing can render JavaScript but recommends not depending on it for important content. Many other crawlers, including a number of AI and social media bots, only read the raw HTML. If visibility beyond Google matters to you, put the content in the initial response.
- Does Googlebot click buttons or scroll?
- No. Googlebot does not click, type or scroll like a user. Content that only loads after a click, a hover or a scroll event is not seen. Use real links for navigation and load lazy content with IntersectionObserver or native lazy loading.
- Is dynamic rendering still recommended?
- No. Google describes dynamic rendering as a workaround and not a long-term solution, and recommends server-side rendering, static rendering or hydration instead.