Skip to content
← Oleksii Turovskyi

Do AI Crawlers Render JavaScript? What Docs and Data Say

· 15 min read · Updated

Listen to this article · 15 min

AI-generated voice

Mostly not. Vercel's December 2024 analysis found the OpenAI, Anthropic, Perplexity, Meta and ByteDance crawlers do not render JavaScript, and OpenAI, Anthropic and Perplexity say nothing about it on their bot pages. The trap is real for client-rendered pages, and prevalence varies widely: 1 of 60 homepages in my scan, 57% of readable sites in We-Amp's 1,000-site study.

Your site looks flawless in a browser. The browser purrs. Open the same URL as a crawler that does not run scripts and you may get <div id="root"></div> and nothing more. The JavaScript trap is a page whose content exists only after scripts run.

TL;DR

  • Vercel's December 2024 analysis found that crawlers from OpenAI, Anthropic, Perplexity, Meta and ByteDance do not render JavaScript. Google documents that Googlebot does.
  • OpenAI, Anthropic and Perplexity are silent on JavaScript on their bot pages, and a report of OpenAI crawlers rendering since September 25, 2026 is unconfirmed.
  • One of 60 homepages in my scan fell below a 0.3 ratio (reddit.com), against 57% of readable sites nearly empty in We-Amp's 1,000-site study. Sample, method and exclusions differ, and I cannot say which explains the gap.

What do bots see that humans do not?

A visitor's browser is a full runtime. It downloads the HTML (HyperText Markup Language), runs the scripts, makes network calls and updates the Document Object Model (DOM).

Most AI crawlers are different. Vercel's analysis of crawler traffic, published December 17, 2024, found that the OpenAI, Anthropic, Perplexity, Meta and ByteDance crawlers do not render JavaScript, while Google's Gemini and Applebot do. According to that analysis, ChatGPT's crawler requested JavaScript files in 11.50% of its requests and Claude's in 23.84%, without executing them.

Press Ctrl+U to see a page the way such a crawler does. If the source holds only <script> tags, a <noscript> message and an empty root <div>, then bingo: nothing to read. Here is the raw HTML of a typical client-side rendering (CSR) app.

index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>My App</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

That body holds no visible text, so against a rendered DOM of two thousand words the ratio of raw to rendered words is 0.

Which AI crawlers execute JavaScript?

So, do AI crawlers render JavaScript? The table shows the position as of October 18, 2026, separating what each operator documents from what Vercel and the Forge AI Bot Log show. For user-agent strings and robots.txt rules, see the AI crawler list.

Crawler What the operator documents What independent data shows
Googlebot Google says a headless Chromium executes the JavaScript and adds that "not all bots can run JavaScript". Vercel: Google's Gemini uses Googlebot's infrastructure for full rendering.
Applebot Apple says it may render pages within a browser. Vercel: renders through a browser-based crawler. Forge: 3,133 script fetches across 4,838 page requests.
CCBot Common Crawl says JavaScript is not executed. Vercel: does not render pages.
GPTBot, OAI-SearchBot, ChatGPT-User OpenAI's bots page does not mention JavaScript. Vercel: none of the three renders. Forge: ChatGPT-User 0 script fetches across 37,045 page requests, OAI-SearchBot 10 across 1,738.
ClaudeBot, Claude-User, Claude-SearchBot Anthropic's support article does not mention JavaScript. Vercel: ClaudeBot does not render. Forge: Claude-User 0 script fetches across 10,028 page requests.
PerplexityBot, Perplexity-User Perplexity's crawlers page does not mention JavaScript. Vercel: PerplexityBot does not render. Forge: PerplexityBot 0 script fetches across 3,193 page requests.

Both independent sources have limits. Vercel counted 569 million GPTBot requests on its network in the month before publication. Forge covers one news site over the 30 days to September 24, 2026, matched bots by name only, and says itself that a log cannot tell reading from running. It also warns that behind a content delivery network (CDN) such as Cloudflare the cache answers most script requests, so a host log can understate script fetches.

One report points the other way for OpenAI. On September 30, 2026, developer Hisashi Space posted logs from one server, matched against OpenAI's published address lists. They show OAI-SearchBot and GPTBot requests jumping on September 25 and shifting to mostly Next.js link prefetches by September 28. He reads that as rendering and found no OpenAI announcement. Forge's window ends the day before, so it cannot test the claim, and I treat the OpenAI row as open.

Where does the 0.3 threshold come from?

From me. It is the cutoff AEO (Answer Engine Optimization) Checker uses to flag a raw-to-rendered ratio below 0.3. It is a heuristic: a round number I picked for a quick check, with no recorded sample behind it. No standards body publishes it, no search engine documents it, and no vendor confirms that crossing it changes anything on their side.

What the ratio is genuinely good for is being cheap. Its raw half is one curl away, and it answers the first question that matters: did any of your content arrive in the response at all? If the raw HTML holds almost none of the visible text, a bot that does not run scripts gets a stub. Read 0.3 as the line where I start looking, not a threshold anyone enforces.

How common is the empty shell?

We-Amp's study Can AI read your website?, published May 28, 2026, measured this on the homepages of the 1,000 most-linked sites in Majestic's referring-domains ranking. It compared the readable content that Mozilla's Readability extracts from the raw HTML with what it extracts from a headless Chromium render. Of the 711 sites it could read, 403 (57%) showed a non-rendering agent "a nearly empty page"; against all 1,000, that is 40%.

On October 18, 2026 I ran a smaller scan of 65 domains: SEO tool vendors, publishers, developer documentation, Ukrainian agencies and e-commerce sites. For each homepage I made one raw request with a Chrome 141 desktop user agent and no JavaScript. I counted the words in <body> after removing script, style, noscript, template and svg elements and comments. I then loaded the page in headless Playwright Chromium, waited for network idle (falling back to page load after 30 seconds), and counted the words in document.body.innerText. A word is a run of letters or digits.

Of the 65, 60 were comparable. I excluded searchengineland.com, stackoverflow.com and nytimes.com (403 to both fetches), rozetka.com.ua (403 to the raw fetch only) and medium.com, whose rendered request returned 403, so its two halves measured different responses. In AEO Checker's three bands, the 60 look like this:

  • Below 0.3 (fail): 1 homepage, reddit.com, with 0 raw words against 1,827 rendered.
  • 0.3 to 0.8 (warning): 2 homepages, prom.ua (0.70) and cloudflare.com (0.75).
  • 0.8 or above (pass): 57 homepages, 95% of the sample.

The median ratio was 1.13, and my own homepage scored 1.24. A ratio above 1 is normal, because the raw HTML includes text a browser hides, such as collapsed menus, mobile navigation and cookie notices, which innerText leaves out; it means nothing is missing, not that bots get extra content. The extension counts textContent on both sides, where my scan used innerText for the rendered side, so the bands match but the ratios are not identical.

reddit.com is the only page below 0.3. I did not check whether it is a client-rendered app or a response tailored to my user agent, but a crawler that skips scripts gets nothing either way.

One reading needs care. vercel.com showed 525 raw words against 73 rendered, a ratio of 7.19. Most likely that was a bot check or client-side state in headless mode, so I treat it as a measurement artifact that passes either way.

My number is far lower than We-Amp's, and I cannot fully explain the gap. The samples differ: it took the most-linked sites, and the blank homepages it names include Instagram, YouTube, Facebook and X ("app front ends, not articles"), none of which is in my 65. The methods differ: Readability extracts article content, so a homepage of navigation and card grids can look thin to it, while a plain word count can pass a shell padded with boilerplate text. The exclusions differ: it set aside 289 of 1,000 sites, almost all blocking automated browsers, and I set aside 5 of 65.

Three sites it flags as falling into the rendering gap are in my 65: reddit.com, shopify.com and bbc.com. Reddit was blank in both scans, but in October Shopify scored 1.47 and bbc.com 1.07 in mine, so method, timing or fetch conditions can change the verdict. It calls its grades directional rather than an official score and publishes no cutoff for "nearly empty," and I have not re-run its sample, so I treat the gap as unresolved.

The scan shows what a client that skips scripts receives, not what any AI crawler fetched or executed. Four limits narrow it further:

  • Homepages only. Inner pages can behave differently.
  • A purposive sample. Mostly large SEO, publishing and developer-tool sites; small business sites and no-code builders are underrepresented.
  • One user agent, one day, one region. Requests came from Ukraine on October 18, 2026, and some sites vary output by user agent or location.
  • Regex counting. A regular expression misses edge cases such as text in attributes, and because the ratio compares counts, hidden text in a raw response could mask missing visible text.

How do SSR and static generation fix it?

The fix is to render the content on the server. Three approaches, each with its own use case:

  • Server-Side Rendering (SSR): HTML generated per request, for dynamic or personalized content.
  • Static Site Generation (SSG): HTML generated at build time and served from a CDN.
  • Incremental Static Regeneration (ISR): static files rebuilt on a schedule or trigger, mixing SSG speed with SSR freshness.

All three put the content in the first response, so a crawler that does not render can read it. The table sets them beside streaming SSR and the two modes that leave such a crawler short.

What each rendering mode hands a crawler that skips JavaScript

Rendering mode First response contains Readable without JavaScript
Static generation (SSG) The full page Yes.
Server-side rendering (SSR) The full page Yes.
Incremental regeneration (ISR) The full page, possibly stale Yes.
Streaming SSR The shell, then content as it resolves Yes, if the content is server-rendered.
Client-side rendering (CSR) An empty <div id="root"> No.
Hybrid with browser-only content The layout without the article No, for the browser-only part.

Readable is not yet citable; the next layer is heading structure.

Why is the Next.js App Router the default I reach for?

This site runs on Next.js with the App Router. Two reasons it suits AI-readable pages:

  1. Server Components by default. Per the Next.js documentation, layouts and pages are Server Components by default, so their output lands in the HTML. A 'use client' file is not client-only, because Next.js still prerenders Client Components to HTML. What stays out of the first response is browser-only work, such as data fetched in an effect (React says effects do not run during server rendering) or a component loaded with ssr: false.
  2. Metadata as code. generateMetadata sets <title>, <meta> and Open Graph tags, which land in the initial HTML when the page can be prerendered and the metadata is not dynamic. Otherwise they can be streamed into <body>. Next.js keeps them in <head> only for user agents on its HTML-limited list, which in the Next.js 16.3.0 install I read excludes GPTBot, ClaudeBot and PerplexityBot.

Here is an article page whose full text is in the first response.

app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation';
import type { Metadata } from 'next';
 
interface Post {
  title: string;
  excerpt: string;
  contentHtml: string;
  publishedAt: string;
  author: string;
}
 
async function getPost(slug: string): Promise<Post | null> {
  const res = await fetch(`https://api.example.com/posts/${slug}`, {
    next: { revalidate: 3600 },
  });
 
  if (!res.ok) return null;
  return res.json();
}
 
export async function generateMetadata(
  { params }: { params: Promise<{ slug: string }> }
): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPost(slug);
 
  if (!post) return { title: 'Not found' };
 
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: 'article',
      publishedTime: post.publishedAt,
      authors: [post.author],
    },
  };
}
 
export default async function PostPage(
  { params }: { params: Promise<{ slug: string }> }
) {
  const { slug } = await params;
  const post = await getPost(slug);
 
  if (!post) notFound();
 
  return (
    <article>
      <header>
        <h1>{post.title}</h1>
        <p>
          <time dateTime={post.publishedAt}>{post.publishedAt}</time>
          {' · '}
          <span>{post.author}</span>
        </p>
      </header>
      <div dangerouslySetInnerHTML={{ __html: post.contentHtml }} />
    </article>
  );
}

Here fetch() runs on the server, so the heading, metadata and body are all in the raw HTML with no 'use client' needed. If you need interactivity, such as a like button, keep it in a separate client component.

app/blog/[slug]/like-button.tsx
'use client';
 
import { useState } from 'react';
 
interface LikeButtonProps {
  postId: string;
  initialLikes: number;
}
 
export function LikeButton({ postId, initialLikes }: LikeButtonProps) {
  const [likes, setLikes] = useState(initialLikes);
  const [isPending, setIsPending] = useState(false);
 
  async function handleClick() {
    setIsPending(true);
    setLikes((n) => n + 1);
 
    try {
      await fetch(`/api/posts/${postId}/like`, { method: 'POST' });
    } catch {
      setLikes((n) => n - 1);
    } finally {
      setIsPending(false);
    }
  }
 
  return (
    <button onClick={handleClick} disabled={isPending}>
      ♥ {likes}
    </button>
  );
}

How do you validate your own rendering?

Check your site in a minute:

  1. Open the page and press Ctrl+U to view the page source.
  2. Search the source for the article's main heading with Ctrl+F, then for a sentence from the body text.
  3. Both found: the content is in the first response. Either missing: you probably have a client-rendering trap.
  4. Confirm it with curl -s -A "GPTBot" https://your-site.com/page | grep -c 'a distinctive phrase'; a count of 0 means the first response lacks the phrase.

If a crawler is blocked in robots.txt, it never reaches this stage; see my earlier post on configuring robots.txt for AI. The manual AI visibility audit covers eight other checks, and this one sits outside them.

Closing

JavaScript frameworks rewrote UX, and the cost lands on pages whose content exists only after scripts run. How many pages that is depends on who measures and how. The vendor pages say nothing about whether each bot will keep skipping scripts, so check your own first response and re-test when a vendor changes its crawlers.

FAQ

Do AI crawlers execute JavaScript?

Mostly not. Vercel's December 2024 analysis found that the OpenAI, Anthropic, Perplexity, Meta and ByteDance crawlers do not render JavaScript. Google documents that Googlebot does, and Apple says Applebot may render pages. OpenAI, Anthropic and Perplexity say nothing about JavaScript on their bot pages, so the evidence is observational and dated.

Can ChatGPT crawl in JavaScript?

Not according to the available data. Vercel's December 2024 analysis saw ChatGPT's crawler request JavaScript files in 11.50% of its requests without executing them. Forge's log to September 24, 2026 shows no script fetches across 37,045 ChatGPT-User page requests. A single-server report of OpenAI rendering since September 25, 2026 is unconfirmed.

Can LLMs read JavaScript?

A language model can read JavaScript source code as text, but reading code is not running it. What matters for a web page is whether the crawler executes scripts to build the page. A model given raw HTML sees script tags, not the content those scripts would have rendered.

Does Googlebot render JavaScript for AI Overviews?

Google's AI features page describes no separate rendering step. It says a page must be indexed and eligible to be shown in Search with a snippet to appear as a supporting link. Googlebot renders pages for indexing, so a script-built page that Google indexes qualifies; that is my inference.

What is the raw-to-rendered ratio, and why 0.3?

It is the word count of the raw HTML divided by the word count of the rendered page. Near 1 means the first response already holds the text, and near 0 means the text arrives later through scripts. The 0.3 cutoff is the one AEO Checker uses, not an industry standard.

Does streaming SSR count as server-rendered?

Yes, as long as the content is part of the server-rendered tree and not fetched in the browser. Streaming changes when bytes arrive within one response, not whether they exist. What breaks a crawler is content that appears in no response at all.

How do I check what a bot actually receives?

Run curl -s https://example.com/page | grep -c 'a distinctive phrase' and expect a number above 0, or disable JavaScript in the browser and reload. Your server log shows whether visiting bots request your script files. The AEO Checker extension compares the raw HTML's word count with the live page's and reports both.

Sources

Part of the Answer Engine Optimization cluster, with the full reading order in dependency sequence, plus a glossary of every term used across these articles.


Has your audit revealed rendering issues on your project? Follow me on LinkedIn to keep up with new AEO tools, or get in touch for a deep audit.

Get new posts

One article every couple of weeks, on AI search and the code behind it. Confirm by email; unsubscribe in one click.