JavaScript SEO: How to See What a Crawler Ends Up With

Google runs your JavaScript. Most other crawlers do not. One comparison shows what a crawler ends up with on your page, and what to change if they differ.

Julian CrossOrganic Growth StrategistSeptember 12, 2026 · 16 min read
Share
/ On this page10 sections

Google runs your JavaScript. Most of the other things reading your pages do not.

One comparison tells you what each of them ends up with, and it takes about two minutes per page. What you do next depends entirely on which side of that split your content sits on.

What JavaScript SEO Is

JavaScript SEO is the work of making sure a search engine ends up with the page you meant to publish, on a site where JavaScript builds or changes what is on it.

It is a slice of technical search engine optimization (SEO), not a separate discipline.

The same titles, links, canonicals and structured data matter as much as they do anywhere else.

What differs is that any of them can be written, overwritten or lost after the server has finished responding. The page your server sends and the page a browser shows become two different objects.

That is the whole of it.

Whether JavaScript Is Costing You Anything

On most sites, no. Almost every site runs JavaScript somewhere, and on most of them none of it decides what a crawler ends up with.

It becomes a search problem only when the content, the links or the directives depend on it. That is a much smaller set of sites than the word "JavaScript" suggests.

Three shapes are worth worrying about.

  • Content that only exists after a script runs. Product grids, prices, review counts, article bodies pulled from an API.
  • Navigation built without real links. Menus, product cards and pagination wired to click handlers rather than to anchors.
  • Head elements written by a script. Titles, canonicals and robots directives set or replaced after the page loads.

If none of those describes your site, spend the afternoon on something with more upside.

If one of them does, the rest of this is for you.

How Google Handles a Page Built With JavaScript

Google processes a JavaScript page in three phases, and they do not all happen at the same moment.

That gap is where most of the trouble lives. Two of the three phases run on the HTML your server sent, and only the third runs on the page a browser built.

The phases are worth knowing in order, because the order is what decides which of your fixes can work.

Crawl, Render, Index

Google's own documentation names the three as crawling, rendering and indexing.

Googlebot requests the URL and reads the response HTML, pulling out links from href attributes and adding them to the crawl queue.

Then the page waits.

Google states the rule for that queue plainly: "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."

Read the second half twice.

It makes the response HTML decide whether rendering happens at all.

A page carrying noindex in the HTML the server sent never reaches the renderer at all. So a script that strips the tag afterwards runs on a page nobody is looking at, and the rendering step you were relying on does not happen.

When the page's turn comes, a headless Chromium executes the JavaScript and produces the rendered HTML. Google parses that for links again and indexes what it finds.

The browser is current. Google says it runs JavaScript with "an evergreen version of Chromium", so the compatibility worries of a decade ago are gone.

How Long Rendering Actually Takes

Not long, on the evidence, and the numbers are worth carrying because they decide whether a workaround is worth building.

Vercel and MERJ measured the gap on over 37,000 rendered pages across a month in 2024 and published the distribution that July. It was 10 seconds at the median, 26 seconds at the 75th percentile, and roughly three hours at the 90th.

The same study analyzed over 100,000 Googlebot fetches and reported that "100% of HTML pages resulted in full-page renders, including pages with complex JS interactions".

Google's own wording agrees in shape. A page "may stay on this queue for a few seconds, but it can take longer than that".

A delay measured in seconds is not worth a workaround. Spend the engineering time on what gets rendered instead.

One exception is real. On a news site, or a price that moves hourly, a page sitting in the tail of that distribution was stale before it was indexed, and server-side rendering buys back the difference.

What Googlebot Will Not Do

Googlebot is a browser with five deliberate limits, and each one is a way a page can look correct to you and arrive empty at Google.

  • It does not keep state. Writing about its Web Rendering Service, Google's troubleshooting guide says "WRS does not retain state across page loads", so local storage, session storage and cookies are cleared between requests. A page that needs a previous visit to populate itself gets nothing.
  • It does not agree to anything. "Expect Googlebot to decline user permission requests," Google writes. Location, camera and notification prompts are all refused.
  • It uses HTTP and nothing else. WebSocket and WebRTC connections are unsupported, so content arriving over either needs an HTTP fallback.
  • It does not support every browser feature. Google asks for feature detection and a polyfill where a needed API is missing, and its own guidance notes that some features cannot be polyfilled at all.
  • It does not click or scroll. Anything that appears only after an interaction is not seen.

None of that is a bug awaiting a fix. It is the shape of the crawler, and building around it is the job.

Distribution of the gap between Googlebot crawling a page and rendering it, drawn in seconds on a logarithmic scale, showing 10 seconds at the median, 26 seconds at the 75th percentile and roughly three hours at the 90th percentile, alongside the finding that every HTML page in the wider sample of over 100,000 Googlebot fetches resulted in a full-page render, Google's own wording that a page may stay on the render queue for a few seconds but can take longer than that, and the one case where the tail still bites, a news page or a price that moves hourly.
Neeraj Jivnani · Vercel and MERJ, How Google handles JavaScript throughout the indexing process, July 2024, delay distribution measured on over 37,000 rendered pages in April 2024
Use this chart — embed code and citation
Embed on your site
<a href="https://neerajjivnani.com/blog/javascript-seo/"><img src="https://neerajjivnani.com/infographics/javascript-seo/render-delay.png" alt="Distribution of the gap between Googlebot crawling a page and rendering it, drawn in seconds on a logarithmic scale, showing 10 seconds at the median, 26 seconds at the 75th percentile and roughly three hours at the 90th percentile, alongside the finding that every HTML page in the wider sample of over 100,000 Googlebot fetches resulted in a full-page render, Google's own wording that a page may stay on the render queue for a few seconds but can take longer than that, and the one case where the tail still bites, a news page or a price that moves hourly." width="1200"></a> <p>Chart: <a href="https://neerajjivnani.com/blog/javascript-seo/">Neeraj Jivnani</a></p>
Cite it
Neeraj Jivnani, "JavaScript SEO: How to See What a Crawler Ends Up With", neerajjivnani.com, https://neerajjivnani.com/blog/javascript-seo/

Free to republish with a link back to this page.

Five cards naming the deliberate limits of Googlebot as a browser: it does not retain state across page loads, so local storage, session storage and cookies are cleared between requests and a page needing a previous visit gets nothing; it declines user permission requests, so location, camera and notification prompts are all refused; it uses HTTP and nothing else, with WebSocket and WebRTC unsupported so content arriving over either needs an HTTP fallback; it does not click or scroll, quoting Google that its lazy-loading methods should not rely on user actions such as scrolling or clicking because Google Search does not interact with your page, so a tab that fetches its content on click is the problem case and infinite scroll is made indexable by supporting paginated loading instead; and it does not support every browser feature, so Google asks for feature detection and a polyfill where a needed API is missing.
Neeraj Jivnani · Google Search Central, Fix search-related JavaScript problems, Understand the JavaScript SEO basics and Fix lazy-loaded content, all read September 2026
Use this chart — embed code and citation
Embed on your site
<a href="https://neerajjivnani.com/blog/javascript-seo/"><img src="https://neerajjivnani.com/infographics/javascript-seo/where-googlebot-stops.png" alt="Five cards naming the deliberate limits of Googlebot as a browser: it does not retain state across page loads, so local storage, session storage and cookies are cleared between requests and a page needing a previous visit gets nothing; it declines user permission requests, so location, camera and notification prompts are all refused; it uses HTTP and nothing else, with WebSocket and WebRTC unsupported so content arriving over either needs an HTTP fallback; it does not click or scroll, quoting Google that its lazy-loading methods should not rely on user actions such as scrolling or clicking because Google Search does not interact with your page, so a tab that fetches its content on click is the problem case and infinite scroll is made indexable by supporting paginated loading instead; and it does not support every browser feature, so Google asks for feature detection and a polyfill where a needed API is missing." width="1200"></a> <p>Chart: <a href="https://neerajjivnani.com/blog/javascript-seo/">Neeraj Jivnani</a></p>
Cite it
Neeraj Jivnani, "JavaScript SEO: How to See What a Crawler Ends Up With", neerajjivnani.com, https://neerajjivnani.com/blog/javascript-seo/

Free to republish with a link back to this page.

The One Comparison That Settles Most of It

Two documents exist for every page on a JavaScript site: the one the server sent, and the one the browser built.

Reading them side by side is the comparison, and it settles most of the questions below before you have to guess at any of them.

It costs about two minutes a page and needs no paid tool.

Start With the Response HTML

Open the page and view its source. That is the response HTML, exactly as it arrived, before a single script ran.

Search it for the elements that decide whether the page can rank at all: the title, the canonical, the robots meta tag, the H1, the body copy, and the internal links.

Whatever is present here is the version Google gets for free, with no rendering, no queue and no chance of failure.

Whatever is missing here is a dependency on rendering. That is not automatically a problem, and it is always worth knowing you have one.

Then Read What Google Rendered

Now get Google's version, from Search Console. The URL Inspection tool returns the rendered HTML, the resources it loaded, and any JavaScript console errors it hit.

Two views are available and they answer different questions.

The crawled version shows what Google holds right now, which is what you want when rankings have already dropped. The live test shows what would happen today, which is what you want after a fix.

Look for three things in the rendered output.

  • Content that is present. Search the rendered HTML for a distinctive sentence from the page body.
  • Resources that were blocked. A robots.txt rule keeping crawlers out of your script or style directories is the commonest cause of a page that renders into nothing.
  • Console errors. A script that threw is a script that stopped, and everything it was going to write never got written.

One warning about the live test. It fetches resources fresh and cuts rendering off early because you are waiting on it, so it is stricter than the real thing, which runs on cached files and is patient.

What a Difference Means

A difference between the two documents is information rather than a verdict. What it costs you depends entirely on which element moved.

What differsWhat it means
Body copy present only after renderingGoogle will index it, and nothing else reading the page will
A link present only after renderingDiscovery waits for the render, and non-rendering crawlers never find the destination
Title or description rewritten by a scriptGoogle uses the rendered value, and your reader sees the old one flash first
noindex in the response, absent after renderingThe page is dropped and never rendered, so the removal never lands
A canonical added or changed by a scriptGoogle is handed two answers and picks one, which may not be yours
Structured data injected after loadEligible, but only if the render succeeded that day

Write the difference down before you take it to anybody.

A ticket that names the element, the URL, the response value and the rendered value is one a developer can act on without reproducing your afternoon.

Where JavaScript Pages Break

Six failures account for most of what goes wrong on a JavaScript site, and every one of them shows up as a difference between the two documents.

They are not ranked below, because the order that matters is the one your own comparison produced. Read the ones your two documents disagreed about and skip the rest.

Links a Crawler Cannot Follow

Google discovers links from one thing, and its documentation is unambiguous: "Google can only discover your links if they are <a> HTML elements with an href attribute."

A JavaScript SEO training deck published by Gray Dot Co and Sitebulb puts two of its own stated takeaways as the shortest way to say it: "No <a href>, no link" and "No <img src>, no image".

So a button that routes, a <span> with a click handler, an href of javascript:void(0), and a card whose whole surface is wired to a router are all invisible as links.

They work perfectly for a person with a mouse. That is what makes them hard to notice.

Fix it in the markup rather than around it. Frameworks emit real anchors when asked, and an anchor can still carry a click handler that makes navigation feel instant.

Where a router genuinely cannot emit anchors, an XML sitemap gets the URLs discovered. It is a fallback for discovery alone, and it does nothing for the internal linking those anchors were also doing.

URLs That Are Not Really URLs

A single-page app can serve every view from one address, or from addresses your server has never heard of. Both break an assumption search engines make.

Fragments are the first case.

Anything after a # is never sent to the server, so example.com/#/products is one URL to a crawler however many views it renders.

Google's documentation names fragment-based routing as a bad practice and the History API as the fix. The History API changes the address bar to a real path the server can also answer.

The second case is the soft 404. A client-side router returns 200 for every path it does not recognize, so a removed product becomes an indexable page saying "not found".

Google publishes two ways out of that. Redirect to a URL the server answers with a real 404, or have the script add a noindex robots meta tag to the failed state.

The Wrong State Gets Rendered

An empty state is the failure worth stopping on, because the page is not missing. It is present, and it is wrong.

A template that renders "No items in this category" while it waits for an API is publishing that sentence. If the render captures that frame, that sentence is what gets indexed, and no human loading the page will ever see it.

The rule we would apply is narrow and easy to hold.

An empty state should render only when the data has arrived and is genuinely empty. While it is still loading, show a skeleton with no indexable copy in it.

A grey box says nothing to a crawler. A sentence says something false.

Directives That Disagree

Titles, descriptions, canonicals, robots tags and hreflang can each be set in the response HTML and then changed by a script. When the two disagree, the outcome is not a coin toss, and it is not always the rendered value that wins.

Google's rendering rule covers the important half already: a noindex in the response HTML stops the page reaching the renderer, so removing it with JavaScript cannot work.

Canonicals behave differently. Google will pick up a canonical injected during rendering, but if the response already carries one and a script adds another, you have handed it two and it decides between them.

Put every directive in the response HTML and leave it there. There is no upside to setting them late, and several ways it goes wrong.

Content That Needs a Click

Two kinds of hidden content behave differently, and only one of them costs you anything.

Accordions and tabs are usually fine. Most implementations put the content in the DOM, the Document Object Model a browser builds from your markup, and hide it with CSS; the ones that fetch on click are the problem.

The test is the same comparison as before: search the rendered HTML for a sentence that lives inside the tab, and see whether it is there.

Infinite scroll has a separate answer. Google's lazy-loading guidance says its recommended methods "don't rely on user actions, such as scrolling or clicking, to load content, which is important as Google Search does not interact with your page".

So the deeper items are found by supporting paginated loading, with a persistent unique URL for each chunk, rather than by the scroll event.

Images, Lazy Loading and Structured Data

Images follow the link rule exactly.

An image referenced from a CSS background, or held in a data-src attribute behind a placeholder, is not an image Google can index.

Native lazy loading solved most of this. The loading="lazy" attribute on a normal <img> element gets you the performance without the JavaScript, and the src stays where a crawler expects it.

Structured data injected with JavaScript is supported, and Google's documentation says so. The condition is that the render succeeded, which puts your rich result eligibility downstream of a script.

If the markup is stable, write it into the response HTML.

Save the injected version for values that genuinely change per request, and test those in the Rich Results Test rather than assuming they arrived.

Weight, and What It Does to Indexing

Weight is a search problem before it is a speed problem, because everything above depends on a render that finishes.

The costs stack in a specific order. A large bundle takes longer to parse and execute, which delays the moment your content exists, which widens the window in which a loading state is the state that gets captured.

Crawl budget is the second cost, and it lands on large sites rather than small ones. Cached resources are cheap for Google to re-fetch; the live API calls made during rendering are not.

The remedy is ordinary performance work.

Split the bundle, drop what you do not use, defer what is not needed for first paint, and read the coverage report in Chrome DevTools to see how much of what you ship is never called.

Core Web Vitals sit alongside this rather than inside it. They measure the experience a person gets, and they are worth fixing for that reason, but a page can score well on all three and still render the wrong content into Google's index.

Choosing How Your Pages Render

Four approaches are in normal use, and the choice belongs to the template rather than to the site. An ecommerce category page and an internal dashboard have opposite requirements, and there is no reason to render them the same way.

ApproachWhat the server sendsWhere it fits
Client-side renderingA near-empty shell plus scriptsDashboards, tools and anything behind a login
Server-side renderingComplete HTML, built per requestPages whose content changes per request and has to rank
Static renderingComplete HTML, built ahead of timeArticles, landing pages, stable category pages
HydrationServer HTML first, then scripts add behaviorMost public pages on a modern framework

Client-side rendering is the only one of the four that puts your content behind a successful script execution.

Google will usually handle it. Every other reader will not, and that is the argument against it rather than any ranking penalty.

Leave Dynamic Rendering Alone

Dynamic rendering, which serves a pre-rendered copy to bots and the normal app to people, is the one approach not in that table, and the one to leave alone.

Google's own page on it opens by saying it "was a workaround and not a long-term solution for problems with JavaScript-generated content in search engines".

The same page names the replacement: "Instead, we recommend that you use server-side rendering, static rendering, or hydration as a solution."

Treat that as settled. Bot detection has to be maintained forever, it drifts out of sync with the real page, and the three alternatives are now the default in Next.js, Nuxt and every comparable framework.

The Readers That Never Run Your JavaScript

Almost everything else that reads your pages takes only the HTML your server sent, and stops there.

Vercel tested it across both Next.js and traditional applications on its own network and published the result in December 2024: "The results consistently show that none of the major AI crawlers currently render JavaScript."

The detail underneath it is the part that surprises people. On the site Vercel measured, ChatGPT's crawlers spent 11.50% of their fetches on JavaScript files and Claude's spent 23.84%, in Vercel's words "despite not executing them".

They are downloading your application and never running it.

Whatever your scripts would have written is absent from what those crawlers take away, and ranking well in Google does not change that.

Two exceptions turned up in the same study. Gemini renders because it runs on Google's infrastructure, and Vercel found that "AppleBot renders JavaScript through a browser-based crawler, similar to Googlebot".

And the Ones Nobody Calls AI

The same limit reaches readers nobody thinks of as AI. A social platform generating a link preview reads Open Graph tags out of the response HTML, and search engines other than Google render inconsistently or not at all.

Google says as much itself: "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".

So the question has changed shape.

It is no longer whether your content is reachable by rendering. It is whether it is reachable without it.

Two columns splitting the things that read a web page by whether they execute JavaScript. On the left, the ones that run it: Googlebot with an evergreen version of Chromium, Gemini because it runs on Google's infrastructure, and AppleBot through a browser-based crawler similar to Googlebot. On the right, the ones that never run it: ChatGPT's crawlers spending 11.50% of their fetches on JavaScript files and Claude's spending 23.84% on the site Vercel measured, despite not executing them, social platforms building a link preview from the Open Graph tags in the response HTML, and other search engines rendering inconsistently or not at all.
Neeraj Jivnani · Vercel, The rise of the AI crawler, December 2024, tested across Next.js and traditional applications on Vercel's network
Use this chart — embed code and citation
Embed on your site
<a href="https://neerajjivnani.com/blog/javascript-seo/"><img src="https://neerajjivnani.com/infographics/javascript-seo/who-runs-your-javascript.png" alt="Two columns splitting the things that read a web page by whether they execute JavaScript. On the left, the ones that run it: Googlebot with an evergreen version of Chromium, Gemini because it runs on Google's infrastructure, and AppleBot through a browser-based crawler similar to Googlebot. On the right, the ones that never run it: ChatGPT's crawlers spending 11.50% of their fetches on JavaScript files and Claude's spending 23.84% on the site Vercel measured, despite not executing them, social platforms building a link preview from the Open Graph tags in the response HTML, and other search engines rendering inconsistently or not at all." width="1200"></a> <p>Chart: <a href="https://neerajjivnani.com/blog/javascript-seo/">Neeraj Jivnani</a></p>
Cite it
Neeraj Jivnani, "JavaScript SEO: How to See What a Crawler Ends Up With", neerajjivnani.com, https://neerajjivnani.com/blog/javascript-seo/

Free to republish with a link back to this page.

Questions People Ask About JavaScript SEO

What is SEO in JavaScript? It is the technical work of getting a search engine to end up with the page you intended, on a site where scripts build or alter the content. Nothing on the checklist is new; what changes is that every item has to be verified against the rendered page as well as the response.

Is JavaScript still relevant in 2026? Yes. What changed is not its relevance but its cost, because content that exists only after a script runs is readable by Google and unreadable by most of what else fetches your pages.

Is SEO dead now with AI? Not on this evidence. The systems people mean by AI cannot execute JavaScript at all, which makes plain crawlable HTML more valuable than it was rather than less.

Does JavaScript hurt rankings? Only when it hides something. Google does not penalize a page for using JavaScript, and pages lose ground when the render is missing content, missing links, or carrying directives that contradict the response.

The Version of Your Page That Matters

When the page your server sent and the page a browser built disagree, what you do about it is a judgment.

Anything a crawler needs to find, understand or trust the page belongs in the response HTML. The title, the canonical, the robots tag, the body copy and the anchors, all of it.

Everything else can wait for the script.

That would have been sound advice ten years ago for different reasons. It is better advice now, because Googlebot has stopped being the only reader that matters and it is still the only one that reliably runs a script.