The Two-Wave Index: When Googlebot Defers JavaScript and What the Rendered DOM Actually Contains
There is a phrase that shows up in almost every JavaScript SEO conversation and almost never in Google’s documentation: “the rendered DOM.” It gets used as if it were a synonym for “what the page contains.” It is not. It is a second artifact, produced later, by a different component, under different conditions than the HTTP response Googlebot first fetches. Treating the two as interchangeable is the root of most indexation debugging errors I see.
Google’s own documentation describes a pipeline with two distinct queues and two distinct parses. The first fetch produces an HTTP response. That response is parsed for links. Later — and “later” is doing real work in that sentence — a headless Chromium renders the page, executes its JavaScript, and produces HTML that Google uses to index. The rendered DOM is not the response. It is what the renderer could produce from the response, given its resources, its caching behavior, and the conditions under which it was allowed to run at all.
This post traces that pipeline from the documented artifacts, maps each documented failure mode to the artifact it produces, and stops where the documentation stops. Where behavior is undocumented, I will say so rather than fill the gap.
Two artifacts, not one
Google’s JavaScript SEO documentation states the model plainly: “Google processes JavaScript web apps in three main phases: Crawling, Rendering, Indexing.” It then adds the sentence that matters most for operators: “Googlebot queues pages for both crawling and rendering. It is not immediately obvious when a page is waiting for crawling and when it is waiting for rendering.”
That is the whole argument in two sentences. There are two queues. A URL can be in the crawl queue, then the render queue, and the operator has no direct visibility into which one it is sitting in at any given moment. The rendered DOM is the output of the second queue, not the first.
The first artifact — the HTTP response — is what Googlebot parses for links in the href attribute of HTML anchors. The second artifact — the rendered HTML — is what Googlebot parses for links again, and what Google uses to index the page. The documentation is explicit: “Googlebot parses the rendered HTML for links again and queues the URLs it finds for crawling. Google also uses the rendered HTML to index the page.”
So a link that exists only after JavaScript runs is discoverable, but only on the second pass. A link that exists in the raw HTML is discoverable on the first pass. Both are documented. The difference is timing, and timing is not free.
What admits a page to the render queue
The admission rules are documented and narrower than most people assume. “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.” And: “All pages with a 200 HTTP status code are sent to the rendering queue, no matter whether JavaScript is present on the page. If the HTTP status code is non-200 (for example, on error pages with 404 status code), rendering might be skipped.”
Two things follow. First, rendering is not a JavaScript-only path. A static HTML page with no scripts still goes through the render queue. That is why the rendered DOM of a static page can differ from its source in ways operators do not expect — the renderer is still doing work. Second, a non-200 status is a documented reason rendering may be skipped. A soft 404 served with a 200 status, by contrast, is admitted to the queue and rendered, which is exactly the failure mode Google’s own guidance on soft 404s in single-page apps is trying to prevent.
The wait is documented but not quantified: “The page may stay on this queue for a few seconds, but it can take longer than that. Once Google’s resources allow, a headless Chromium renders the page and executes the JavaScript.” That is the entire public statement on render latency. Anyone quoting a specific number is quoting something else.
There is also a hard gate before the fetch even happens. “Google Search won’t render JavaScript from blocked files or on blocked pages.” A robots.txt disallow on a script file means the renderer cannot execute that script, and the content it would have produced does not appear in the rendered DOM. The page may still be crawled; the JavaScript will not run.
Why “deferred” is structural, not incidental
The render queue is gated on Google’s resources. That is the documented mechanism, and it has a consequence the documentation does not spell out but the model implies: the rendered DOM is not guaranteed to be current relative to the crawl. A page can be fetched on Monday and rendered later, and the renderer’s view of the page is whatever the page served at render time, subject to the caching behavior described below. Operators who reason about “what Google sees” as a single moment are reasoning about a pipeline that does not have a single moment.
This is the part of the model that deserves the most skepticism toward vendor narratives. “Google renders JavaScript” is true and incomplete. Google renders JavaScript when its resources allow, from resources it was permitted to fetch, on pages that were admitted to the queue, with caching behavior that may serve stale assets. Each of those clauses is a documented condition, and each one is a place where the rendered DOM can diverge from what a developer sees in a browser.
What the rendered DOM actually contains
The documentation describes several things the renderer produces that operators should expect to find in the rendered HTML, and several things it does not.
What it contains
Executed JavaScript output. Content generated by client-side code appears in the rendered HTML, subject to the execution conditions above. This is the baseline case the whole pipeline exists to handle.
Flattened light and shadow DOM. “When Google renders a page, it flattens the shadow DOM and light DOM content. This means Google can only see content that’s visible in the rendered HTML.” The documentation’s web component example shows the flattened output explicitly, and warns that if content is not visible in the rendered HTML, Google will not be able to index it. The <slot> mechanism is the documented way to make light DOM content appear in the flattened output.
JavaScript-injected canonical, meta, and structured data. The documentation states that Google Search will pick up an injected rel="canonical" link tag when rendering the page, and that JavaScript can set or change the <title>, meta description, robots meta tag, and JSON-LD. It also warns that multiple or conflicting canonical tags may lead to unexpected results, and that the best practice is to set the canonical in HTML and, if JavaScript must be used, to set it to the same value as the original HTML.
What it does not contain
State from cookies or storage. “WRS does not retain state across page loads: Local Storage and Session Storage data are cleared across page loads. HTTP Cookies are cleared across page loads.” Content that depends on a logged-in session, a stored preference, or a cookie-gated variant will not appear in the rendered DOM unless the page produces it without that state.
Content delivered over non-HTTP transports. “Googlebot uses HTTP requests to retrieve content from your server. It does not support other types of connections, such as WebSockets or WebRTC connections.” Content that arrives only over a WebSocket does not reach the renderer.
Permission-gated features. “Features that require user permission don’t make sense for Googlebot, or for all users. For example, if you make the Camera API required, Googlebot can’t provide a camera to you.” The documentation also notes that Googlebot does not support WebGL, and recommends feature detection with a fallback or server-side rendering for content that depends on it.
Anything behind a noindex the renderer respects. “When Google encounters the noindex tag, it may skip rendering and JavaScript execution, which means using JavaScript to change or remove the robots meta tag from noindex may not work as expected.” A page that ships noindex in the original HTML and removes it with JavaScript is asking the renderer to run code it may have already decided not to run.
The caching wrinkle
One documented behavior deserves its own section because it changes what “the rendered DOM” means in practice. “Googlebot caches aggressively in order to reduce network requests and resource usage. WRS may ignore caching headers. This may lead WRS to use outdated JavaScript or CSS resources.”
The documented mitigation is content fingerprinting: put a hash of the file’s content in its filename, so that an update produces a new URL and the cache cannot serve the old one. The documentation gives the example main.2bb85551.js. This is not an optimization. It is a build-pipeline requirement that follows directly from the documented caching behavior. If your asset filenames do not change when the content changes, the renderer may execute a version of your code that no longer matches your source.
The evergreen baseline is a dated claim, not a current version
In May 2019, Google announced that Googlebot runs “the latest Chromium rendering engine (74 at the time of this post)” and “will regularly update its rendering engine to ensure support for latest web platform features.” The post lists ES6 and newer JavaScript features, IntersectionObserver for lazy-loading, and Web Components v1 APIs among the 1000+ new features supported, and advises checking whether transpiling or polyfills aimed specifically at Googlebot are still necessary.
That post is a dated baseline, not a current version claim. The documentation does not publish a current Chromium version for Googlebot, and anyone asserting one is asserting something the sources do not support. What the post does establish is the policy: the renderer tracks a recent Chromium, and polyfill decisions aimed at Googlebot should be re-evaluated against current documented support rather than historical assumptions. The 2019 post tells you the direction of travel; it does not tell you what the renderer supports today.
The AJAX crawling scheme is gone, and dynamic rendering is a workaround
Two historical artifacts are worth naming because they still shape assumptions. The AJAX crawling scheme, which accepted #! URLs and ?_escaped_fragment_= versions, was retired: Google announced in December 2017 that “in the second quarter of 2018, we’ll be switching to rendering these pages on Google’s side, rather than on requiring that sites do this themselves.” The escaped-fragment contract is not a thing to build against.
Dynamic rendering — serving crawlers a server-rendered version while users get the client-rendered version — is described in current documentation as “a workaround and not a long-term solution,” with server-side rendering, static rendering, or hydration recommended instead. The documentation also notes that dynamic rendering is generally not considered cloaking as long as it produces similar content, and that serving completely different content to users and crawlers can be cloaking. If a vendor is pitching dynamic rendering as a strategy rather than a stopgap, the documentation does not support that framing.
What this means for crawl budget
The crawl budget documentation defines a site’s budget as the product of a crawl capacity limit and crawl demand. The capacity limit “limits the total amount of time your server spends holding connections open for Google, factoring in both the number of parallel connections and their duration.” Demand for Googlebot “varies based on a site’s size, update frequency, page quality, and relevance, compared to other sites.”
The documentation also states that “if Google can load and render your pages faster, we might be able to read more content from your site,” and that rendering is gated on Google’s resources. What the documentation does not provide is any quantitative account of how much crawl budget rendering consumes, or how the render queue is scheduled relative to the crawl queue. That is undocumented, and I am not going to invent a number for it. What can be said is structural: rendering is a separate queue with its own resource constraints, and the rendered DOM is the artifact that queue produces.
One more documented point worth keeping straight: “crawling is necessary for being in the results, it’s not a ranking signal.” Crawl budget affects discovery and freshness, not position directly. The two-queue model is a discovery and rendering model, not a ranking model.
An operator checklist derived from documented behavior
Everything below follows from the sources cited above. None of it is a claim about how any specific site will behave.
- Verify against the rendered HTML, not the source. The documentation recommends the URL Inspection Tool and Rich Results Test to see loaded resources, JavaScript console output, rendered DOM, and more. If content is not in the rendered HTML, Google cannot index it.
- Check what is blocked before you check what is rendered. A
robots.txt disallow on a script file prevents execution. The rendered DOM will not contain what that script would have produced.
- Do not rely on JavaScript to remove a noindex. The documentation states the renderer may skip rendering and execution when it encounters noindex, so JS-based removal may not work as expected. If you want the page indexed, do not ship noindex in the original HTML.
- Keep the canonical consistent between HTML and JavaScript. If JavaScript must set it, set it to the same value as the original HTML, and ensure only one canonical tag exists.
- Fingerprint asset filenames. WRS may ignore caching headers and serve stale JavaScript or CSS. Content-hashed filenames are the documented mitigation.
- Do not depend on cookies, local storage, or session storage for content you want indexed. WRS clears them across page loads.
- Do not depend on WebSockets, WebRTC, or permission-gated APIs for indexable content. Googlebot uses HTTP and cannot grant permissions.
- Check web component output in the rendered HTML. Shadow and light DOM are flattened; content not visible in the flattened output is not indexable. Use
<slot> where the documentation recommends it.
- Re-evaluate Googlebot-specific polyfills. The evergreen policy means the renderer tracks a recent Chromium. The 2019 post is a dated baseline; check current documented support rather than assuming an old version.
- Treat render lag as a variable. The render queue wait is documented as “a few seconds, but it can take longer.” The rendered DOM is not guaranteed to be current relative to the crawl.
FAQ
Does Googlebot render every page it crawls?
No. Pages with a 200 status are queued for rendering unless a robots meta tag or header says not to index. Non-200 responses may skip rendering. Pages blocked by robots.txt are not fetched, and JavaScript from blocked files or on blocked pages is not rendered.
How long does a page wait in the render queue?
The documentation says “a few seconds, but it can take longer than that.” It does not publish a specific figure, and any specific number quoted elsewhere is not from the documentation.
Does the rendered DOM include content behind a login?
Not from session state. WRS clears cookies, local storage, and session storage across page loads. Content that requires a logged-in session will not appear in the rendered DOM unless the page produces it without that state.
Can Google index content that only appears after a WebSocket message?
No. Googlebot uses HTTP requests and does not support WebSockets or WebRTC connections. Content delivered only over those transports does not reach the renderer.
Is dynamic rendering still recommended?
The current documentation describes dynamic rendering as “a workaround and not a long-term solution” and recommends server-side rendering, static rendering, or hydration instead.
What Chromium version does Googlebot use?
The documentation does not publish a current version. The 2019 announcement stated version 74 at the time of that post and committed to regular updates. That post is a dated baseline, not a current version claim.
Where the documentation stops
The two-queue model is documented. The admission rules are documented. The contents of the rendered DOM — executed JavaScript, flattened shadow and light DOM, injected canonical and meta tags — are documented. The exclusions — state, non-HTTP transports, permission-gated APIs, noindex — are documented. The caching behavior and its mitigation are documented.
What is not documented is the scheduling relationship between the two queues, the resource model that gates rendering, or any quantitative account of render latency or crawl-budget consumption by rendering. Those are the places where the honest answer is “the documentation does not say,” and the useful move is to stop there rather than fill the gap with a number that sounds authoritative. The rendered DOM is a real artifact with real failure modes. Treating it as a synonym for “the page” is how operators end up debugging the wrong artifact.