
A page can look perfect in Chrome and still be empty to the system that decides whether it ranks. That gap is where every JavaScript SEO problem lives.
In 2026 the gap has a second edge. Google renders your JavaScript reliably. The crawlers behind ChatGPT, Claude and Perplexity do not render it at all, so a client-side rendered product page can rank on Google and be invisible to every AI answer at the same time.

This guide covers both halves: how Google actually processes JavaScript, how to see what it rendered, and the fixes that recover rankings. We have been running technical SEO audits on JavaScript-heavy sites for years, and the same handful of failures account for almost every case.
How Google processes your JavaScript
Google does not process a JavaScript page in one pass. Its own documentation describes three phases: crawling, rendering and indexing, and each one can fail independently.
Phase 1: crawling
Googlebot checks robots.txt first. If the URL is disallowed it skips the HTTP request entirely, which a robots.txt tester will confirm in seconds. If your JavaScript bundle or API endpoint is blocked, rendering cannot work either.
It then fetches the HTML and parses it for links in href attributes. On a client-side rendered app that initial HTML is often an empty shell with a <div id="root">, so Google finds nothing to queue until rendering happens. Always emit real <a href=""> tags, even with JavaScript routing.
Phase 2: rendering
Every page that returns a 200 goes into a render queue. Google's wording is deliberately vague: the page "may stay on this queue for a few seconds, but it can take longer than that."
The queue then hands the page to the Web Rendering Service, which uses an evergreen version of Chromium. That means current browser APIs are generally available, but not that every resource will load or that a slow API call will be waited for.
The old rule of thumb was a hard five-second timeout. Google has never published a number, and the five seconds that gets quoted was Martin Splitt describing how long a page typically waits in the queue, not how long the renderer waits for your API. Treat five seconds as a budget for your critical content, not a wall, and do not design around either.
Phase 3: indexing
Only after rendering does Google see your real content, links and meta tags. The failures we see most at this stage are soft 404s on single-page apps, noindex or canonical tags injected by JavaScript that conflict with the source HTML, and content that rendered but never appeared because it was gated behind a click or a permission prompt.
What changed in 2026
Three things are worth knowing before you audit anything.
Google has become more confident, and says so. On 4 March 2026 it removed the "Design for accessibility" section from its JavaScript SEO documentation, explaining that "Google Search has been rendering JavaScript for multiple years now, so using JavaScript to load content is not 'making it harder for Google Search'" (Search Engine Land). The two-wave rendering delays of 2018 are not the practical problem any more.
Dynamic rendering is officially a workaround. Google's documentation now calls it "a workaround and not a long-term solution" and recommends server-side rendering, static rendering or hydration instead. If a vendor is still selling you a prerender proxy as the fix, it is 2019 advice.
AI crawlers do not run JavaScript. Vercel's analysis of roughly 1.3 billion AI crawler fetches on its network found that GPTBot and ClaudeBot fetch JavaScript files (in 11.5% and 23.8% of requests respectively) but do not execute them, and none of the AI crawlers it tracked rendered client-side content. Glenn Gabe's August 2025 test asked ChatGPT, Perplexity and Claude to read pages from a client-side rendered site, and all three said they could not access the content, while the same pages served with server rendering worked.
| Crawler | Executes JavaScript | What that means for a client-side rendered page |
|---|---|---|
| Googlebot (Search, AI Overviews, AI Mode) | Yes, evergreen Chromium | Content is indexed after rendering, with a delay |
| GPTBot and ChatGPT's fetchers | No | ChatGPT sees the empty shell |
| ClaudeBot and Claude's fetchers | No | Claude sees the empty shell |
| PerplexityBot | No | Perplexity sees the empty shell |
The consequence for React, Next.js, Vue and Angular teams is blunt. Anything you want quoted, cited or recommended by an AI assistant has to be in the initial HTML response. We covered why this is a debt problem rather than a content problem in AI visibility technical debt, and what ChatGPT's fetcher actually reads in ChatGPT's index and cache. If you are also deciding which of those bots to allow, our AI crawlers robots.txt guide covers the user agents.
Quick diagnostic process
Before touching code, find out what each crawler actually received.
1. Look at the rendered HTML, not your browser
Chrome DevTools shows you what your logged-in browser rendered with cookies, storage and a warm cache. Googlebot has none of that.
Use the URL Inspection tool in Search Console, run Test live URL, then open View tested page. That panel gives you a screenshot of the rendered page, the raw HTML returned, the HTTP headers, the JavaScript console output and every page resource loaded. The screenshot only exists on a live test, and the console output is where you find the error that only happens for Googlebot.
Compare that rendered HTML against your view-source. If the content is in one and not the other, you have a rendering problem. If it is in neither, you have a build problem. For a fast first pass on a single URL, our Chrome SEO audit extension flags the on-page basics without leaving the page, and the Rich Results Test remains a quick way to see rendered structured data.
2. Look at the raw HTML the AI crawlers get
This one takes ten seconds and most audits skip it:
curl -s -A "GPTBot" https://www.example.com/your-product-page/ | grep -c "your product name"
If the count is zero, ChatGPT cannot see that product. Repeat with a few important templates. There is no rendering step to debug here, because none of those crawlers render.
3. Log the errors Googlebot hits
Google's troubleshooting guide includes a global error handler that ships console errors to your own endpoint. We put a version of it on every JavaScript-heavy site we work on:
window.addEventListener('error', function (e) {
var errorText = [
e.message,
'URL: ' + e.filename,
'Line: ' + e.lineno + ', Column: ' + e.colno,
'Stack: ' + (e.error && e.error.stack || '(no stack trace)')
].join('\n');
var client = new XMLHttpRequest();
client.open('POST', 'https://your-error-tracker.example/log');
client.setRequestHeader('Content-Type', 'text/plain;charset=UTF-8');
client.send(errorText);
});
Filter the logs by the Googlebot user agent. The errors that appear only there are usually a blocked resource, a missing polyfill or a permission prompt.
4. Check at scale
One URL tells you about one template. To check thousands of pages in a single crawl, our guide to Screaming Frog custom JavaScript shows how to run your own checks against the rendered DOM, including whether an H1 is visible after render and how much content sits behind toggles.
The JavaScript SEO fixes that recover rankings
Everything below maps to a specific limitation in Google's rendering pipeline, and every one is documented in Google's own troubleshooting page.
1. Soft 404s in single-page apps
Your router shows a "not found" view but the server returned 200. Google indexes the empty page, Search Console reports a soft 404, and your error template can end up ranking for brand queries.
Google gives two options. Redirect to a URL where the server responds with a real 404:
fetch(`/api/products/${id}`)
.then(res => res.json())
.then(product => {
if (!product.exists) {
window.location.href = '/not-found';
}
});
Or add a noindex robots meta tag from JavaScript when the content does not exist:
fetch(`/api/products/${id}`)
.then(res => res.json())
.then(product => {
if (!product.exists) {
const metaRobots = document.createElement('meta');
metaRobots.name = 'robots';
metaRobots.content = 'noindex';
document.head.appendChild(metaRobots);
}
});
The first option is better. It fixes the status code for every crawler, not just the one that renders.
2. Permission prompts
Google's guidance is to "expect Googlebot to decline user permission requests." If your content only appears after the user grants location, camera or notification access, Googlebot never sees it.
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(showLocalProducts, showDefaultProducts);
} else {
showDefaultProducts();
}
function showDefaultProducts() {
// Runs for Googlebot and for users who decline.
// Put the content you need indexed here.
}
Store locators and delivery-area checks are the usual culprits.
3. Fragment URLs
Routing with example.com/#/products still shows up in audits. The AJAX crawling scheme has been deprecated since 2015, and Google's guidance is to use the History API instead:
// Instead of: window.location.hash = '/products'
history.pushState({ page: 'products' }, 'Products', '/products');
Every modern router does this by default. If yours does not, that is the migration to schedule first.
4. No state between page loads
The Web Rendering Service does not retain state. Local Storage, Session Storage and cookies are all cleared between page loads. Any logic like this fails silently for Googlebot:
// Breaks for Googlebot
if (localStorage.getItem('userAuthenticated')) {
loadPremiumContent();
}
Render the public version of the content by default and layer the personalised version on top.
5. Content fingerprinting
Google says Googlebot "caches aggressively in order to reduce network requests and resource usage," and it may ignore your cache headers. The fix is to put a hash of the file contents in the filename:
// webpack, Vite and Next.js all do this by default
output: {
filename: '[name].[contenthash].js'
}
// Produces main.2bb85551.js, which changes on every build
If you serve app.js with no fingerprint, Googlebot can keep running an old bundle long after you have shipped the fix.
6. Feature detection
Googlebot's Chromium is current, but headless rendering still differs from a user's browser. Google asks you to feature-detect every critical API and provide a fallback or polyfill:
if (window.WebGLRenderingContext) {
renderWithWebGL();
} else {
renderStaticFallback();
}
The failure mode is a bundle that throws before it reaches your content. One uncaught exception at the top of the app and nothing below it renders.
7. HTTP only
Googlebot uses HTTP requests and does not support WebSockets or WebRTC. If content arrives over a socket, add an HTTP fallback:
function initialiseDataStream() {
if ('WebSocket' in window) {
connectWebSocket();
} else {
setInterval(fetchDataViaHttp, 5000);
}
}
Live pricing widgets and chat-style interfaces are where this bites.
8. Web components and shadow DOM
The Web Rendering Service flattens the light DOM and shadow DOM. Components that do not use the <slot> mechanism for light DOM content can lose that content in the rendered HTML. Check the rendered output in URL Inspection, and if content is missing, switch to slot-based projection or render the critical content on the server.
9. The noindex trap
Google says that when it encounters a noindex tag it "may skip rendering and JavaScript execution." So this never works:
// Too late: Google may not have rendered the page to run this
var metaRobots = document.querySelector('meta[name="robots"]');
metaRobots.setAttribute('content', 'index, follow');
Never ship noindex in the initial HTML if JavaScript is supposed to remove it. The same logic applies to canonicals and hreflang: JavaScript can add them, but a conflicting value in the source HTML wins the argument. That is a frequent cause of broken international SEO setups, and why we tell teams running hreflang across multilingual sites to emit those tags server-side.
10. Lazy loading and infinite scroll
Google's lazy loading guidance is explicit that "Google Search does not interact with your page." It does not scroll, click or hover. Anything that needs a user action to load never loads for Googlebot.
Use native loading="lazy" on images and iframes, or IntersectionObserver, and check that image URLs appear in the src attribute of the rendered HTML. For infinite scroll, give each chunk of content its own unique URL (for example ?page=12), link the pages sequentially, and update the URL with the History API as chunks load.
11. Links that are not links
Anything that navigates through onclick, a javascript: href or a <span> with a router handler is invisible during the crawl phase. Crawl the site once with rendering off and compare the link counts against a rendered crawl; the difference is the set of pages Google can only discover after rendering, if at all.
Framework-specific advice for 2026
The frameworks have largely solved this, provided you use them the way they now ship.
| Framework | Default in the current major version | What to check |
|---|---|---|
| Next.js (App Router) | Layouts and pages are Server Components by default, rendered to HTML on the server | Every 'use client' file you add pushes that content out of the initial HTML. Keep product text, headings and links in Server Components |
| Nuxt 4 | Universal rendering (server-rendered, then hydrated) | Route rules with ssr: false turn off server HTML for that route. Use prerender or isr for content pages instead |
| Angular | SSR through @angular/ssr, with incremental hydration enabled by default when you use provideClientHydration() |
Content inside @defer blocks with hydrate never still renders on the server. Content loaded in ngOnInit from an API does not |
| Create React App, Vite SPA, Vue CLI SPA | Client-side only | The whole page depends on rendering. Migrate content routes to a server-rendering framework or a static build |
The pattern across all three is the same. Server rendering is now the default and the SEO problems come from opting out of it, usually one component at a time, until the primary content of a template has quietly moved to the client. A quarterly crawl with rendering off is the cheapest way to catch that drift.
If you cannot move to server rendering this quarter, the order of fallbacks is static generation for the pages that earn traffic, then hybrid rendering (server-rendered content, client-side interactions). Dynamic rendering for bots is last, and Google now says so itself.
Verification
After a fix ships, check it in the order the crawlers will.
- Run Test live URL in URL Inspection and confirm the content, links and meta tags are in the rendered HTML and the console is clean.
- Curl the page as GPTBot and confirm the primary content is in the raw response.
- Request indexing for the affected templates and watch the Pages report over the following weeks.
- Keep the error logging in place. New releases reintroduce old problems.
Where this leaves you
JavaScript SEO in 2026 is two problems with one fix. Google will render what you ship, eventually, as long as you do not gate it behind a click, a permission or a stale bundle. AI crawlers will not render anything, so the content you care about has to be in the HTML response. Server rendering the primary content of every template satisfies both, and the modern versions of Next.js, Nuxt and Angular do it by default.
If you would rather have someone walk your stack template by template, that is what our technical SEO service and website audits are built for, and a free SEO review is a sensible place to start. Search is spreading across Google, ChatGPT and Perplexity, and the sites that render on the server are the only ones present in all of them.





