JavaScript execution has two depths, and bots split cleanly between them
Table of contents
We already knew NotebookLM runs JavaScript and Bingbot splits into a JS and a non-JS subsystem. Both findings came from one beacon firing, or not firing. This one comes from the beacon firing through the wrong channel - and it turns “does the bot run JS?” into “how deep does it go?”
Two channels
The Layer 3 beacon has two independent delivery paths:
- Inline - an inline
<script>at the end of<body>, sent as an image request. Fires if the bot executes raw inline script. - Client-component - a React client component running in a
useEffect, sent as afetch(). Fires if the bot evaluates the React bundle and runs the component lifecycle.
Both ran in parallel the whole window (the last inline fire was 2026-08-01). So “fired one channel but not the other” is a property of the renderer, not an artifact of timing - in any week where one bot fired the inline channel, another fired the component channel.
Four tiers
| tier | classes | inline | component | reading |
|---|---|---|---|---|
| Bundle-evaluators | applebot, googlebot (WRS), google-readaloud, google-other, notebooklm | 0 | 54 | Evaluate the React tree, skip the inline <body> script |
| Inline-only | gptbot | 3 | 0 | Runs a raw inline script, does not evaluate the bundle |
| Full | human, headless-chrome, gsc-inspection, bingbot | 122 | 65 | Both channels - full browser-class execution |
| Zero | claudebot, amazonbot, ccbot, meta-external, oai-searchbot, perplexitybot, chatgpt-user, claude-user, perplexity-user, google-extended, bytespider | 0 | 0 | No beacon, ever |
Two headline numbers
Applebot renders almost everything it crawls. 32 component-channel beacons against 32 verified Applebot cell fetches, spread across all eight content patterns. Applebot belongs with Googlebot-WRS in the full-renderer club, not with the batch AI crawlers.
GPTBot is the only batch AI crawler that runs any JS at all - and only the inline channel (three fires, one cell, one day). Every other batch AI crawler produced exactly zero beacons across 80 days.
Why the beacon population is trustworthy
A spoofer sending a fake user-agent to a static scraper cannot fire the beacon - firing it requires actually executing the page’s JS. So this layer self-selects for genuine renderers; the nine-IP impersonation pool that pollutes the HTTP logs never appears here. The beacon, not any HTTP side-effect, is the authoritative execution signal.
What this means for site owners
The tiers imply a specific rule for JS-gated content:
- Injected by an inline
<script>: seen by GPTBot, skipped by the renderers that matter most (Google WRS, Applebot, Read Aloud, NotebookLM), which treat the inline<body>script as boilerplate. - Behind React (
useEffect, clientfetch, hydration): seen by Applebot, Googlebot-WRS, Read Aloud and NotebookLM; invisible to GPTBot and every zero-JS batch crawler. - In the server-rendered HTML: seen by everyone.
If you must hide content behind JavaScript, put it in the React component lifecycle, not an inline script. But the only channel that reaches the zero-JS majority - ClaudeBot, Perplexity, Amazon, CCBot - is server-side rendering.
What this does not prove
- The mechanism is confounded. The inline channel is both an inline script and an image-beacon, so we cannot fully separate “skips the inline script” from “blocks analytics-shaped image requests.” The effect on content visibility is the same; the cause is not isolated.
- GPTBot’s inline result is thin (n=3, one cell, one day). Directional, not robust.
- The beacon is a floor, never a ceiling. A bot can render fully and still drop the beacon (sandbox, keepalive). Every tier is a lower bound on execution.
What would falsify this
If, in a fresh window, Applebot starts firing the inline channel or GPTBot fires the component channel, the tiers are less clean than they look here. And if a claudebot or amazonbot beacon ever appears, the zero tier breaks - that single row would be a bigger finding than this whole post.
Method notes
Beacon rows are stored with a source of inline or client-component; the counts above are direct group-bys over the js_executions table for the full window. Applebot’s render rate compares its 32 component beacons against its 32 verified (reverse-DNS confirmed) content fetches in the canonical HTTP log - aggregate counts, not a per-request join, so read it as “near 1:1,” not exact.
Data availability: methodology, tracker code, classifier and beacon source are public at github.com/Qbeczek1/jsseo-dev.
Bias disclosure: I run JS SEO Lab as an independent technical SEO research project, and do paid technical SEO and AI-visibility audits through FratreSEO. No framework vendor, crawler vendor, search engine, or AI company funds this work.