JavaScript SEO: how Google renders pages built with JS
Google can run JavaScript, but it does so in a queue, without clicking or scrolling, and with a few limits a normal browser doesn't have. This guide explains how Google crawls, renders and indexes JavaScript pages, and what to check in links, titles, status codes, lazy loading and rendering so your content still gets found.
This guide is built from Google Search Central's Understand the JavaScript SEO basics (last updated 4 March 2026), its troubleshooting guide Fix Search-related JavaScript problems, and the pages on lazy-loaded content, dynamic rendering and crawlable links. Whether your project should be a website or a web app in the first place is covered in our web app vs website guide; this page is about getting the JavaScript parts of either one indexed.
- Crawl, render, index
- Links and routing
- Titles, canonicals and robots tags set by JavaScript
- Status codes and soft 404s in single-page apps
- What Google's renderer does differently
- Lazy loading and infinite scroll
- Server-side rendering and dynamic rendering
- Testing a JavaScript page
- How we approach it
- Frequently asked questions
Crawl, render, index
Google processes JavaScript web apps in three phases: crawling, rendering and indexing. Google Search "runs JavaScript with an evergreen version of Chromium", so modern code is not the problem in itself. What matters is the order things happen in.
- Crawling. Googlebot takes a URL from its queue, checks robots.txt, and fetches the page if it is allowed. If robots.txt disallows the URL, Google skips it, and it "won't render JavaScript from blocked files or on blocked pages". It then reads the links in the HTML response and queues them.
- Rendering. Every page that returns a 200 status code goes to the rendering queue, whether or not it uses JavaScript, unless a robots meta tag or header says not to index it. A page "may stay on this queue for a few seconds, but it can take longer than that". A headless Chromium then renders the page and runs the scripts.
- Indexing. Google reads the rendered HTML for more links and uses that rendered HTML to index the page.
For a classic or server-rendered site, the first HTML response already holds the content. With the app shell model, where the first HTML is mostly empty, Google has to run your JavaScript before it sees anything. Google's advice is that "server-side or pre-rendering is still a great idea" because it makes the site faster for users and crawlers, and "not all bots can run JavaScript". On non-200 pages, such as a 404, rendering might be skipped.
Links and routing
Google can only find your links reliably "if they are <a> HTML elements with an href attribute". JavaScript may add links to the page after load; that is fine as long as the result is that same markup. The general rules on link markup and anchor text are in our internal linking guide. The patterns that turn up in JavaScript frameworks are:
<a href="/products">, also with anonclickhandler added: Google can parse it.<a routerLink="products/category">,<span href="...">or<a onclick="goto(...)">: not recommended. Google may still try, but can't extract these reliably.<a href="javascript:goTo('products')">: not recommended, because the href doesn't resolve to a real web address.
Single-page apps also need real URLs for each view. Google tells you not to use URL fragments such as example.com/#/products to load different content: the old AJAX crawling scheme "has been deprecated since 2015", so fragments can't be relied on. Use the History API instead, with normal paths in the href and pushState to update the address bar when your script swaps the content in.
Titles, canonicals and robots tags set by JavaScript
You can use JavaScript to set or change the <title> element and the meta description. Each view should still get its own unique, descriptive title and description.
The canonical tag has stricter rules. Google says the best way is plain HTML. If you must use JavaScript, set the canonical to the same URL as in the original HTML, or leave it out of the HTML and add it only with JavaScript. Don't use JavaScript to change it to a different URL, and make sure the injected tag is the only rel="canonical" on the page, since "conflicting or multiple rel="canonical" link tags may lead to unexpected results". Our canonical tags guide explains how Google chooses the canonical URL.
Robots tags are where JavaScript catches people out. You can add a noindex with JavaScript, for example when an API call returns nothing. You can't reliably do the opposite. When Google sees a noindex, "it may skip rendering and JavaScript execution", so a script that removes the noindex may never run. If you want a page indexed, don't put noindex in the original page code.
Structured data can be generated in JavaScript too: Google accepts JSON-LD injected into the page, as long as you test it. See our structured data basics.
Status codes and soft 404s in single-page apps
Googlebot uses HTTP status codes to tell whether something went wrong, for example a 404 for a missing page or a 401 for a page behind a login. Single-page apps often handle routing in the browser, so the server answers 200 for every URL, including products that no longer exist. Google warns that this "can lead to error pages being indexed and possibly shown in search results". Google calls these soft 404 errors.
Google gives two fixes, and you can use one or both:
- Redirect with JavaScript to a URL where the server returns a real 404, such as
/not-found. - Add
<meta name="robots" content="noindex">to the error view with JavaScript.
How 404s and redirects should work on the server side is covered in our 404s and redirects guide.
What Google's renderer does differently
Google's rendering is done by the Web Rendering Service (WRS). Its troubleshooting guide lists where it behaves unlike a visitor's browser:
- No permissions. Expect Googlebot to decline permission requests. If content needs camera access, for example, Googlebot won't see it, so give users a way in without it.
- No memory between pages. Local storage, session storage and HTTP cookies are cleared across page loads. Content that depends on saved state won't be there.
- HTTP only. Googlebot fetches over HTTP. It doesn't support WebSockets or WebRTC, so content loaded that way needs an HTTP fallback.
- Missing features. Some web features aren't supported; Google's example is WebGL. Use feature detection with a fallback or polyfill, or render that content on the server.
- Old files from cache. Googlebot "caches aggressively" and the WRS may ignore caching headers, so it can use outdated JavaScript or CSS. Google's fix is content fingerprinting: file names like
main.2bb85551.jsthat change whenever the file changes. - Web components. Google flattens the shadow DOM and light DOM and can only index what ends up in the rendered HTML. Use a
<slot>for light DOM content. - Skipped requests. Googlebot may not fetch resources that don't contribute to the main content, such as reporting or error requests. As a result, Google notes, client-side analytics "may not provide a full or accurate representation" of Googlebot activity.
- Paywalls. Sending the full text and hiding it with JavaScript until a subscription is confirmed "isn't a reliable way to limit access". Send the full content only after the check.
Lazy loading and infinite scroll
Lazy loading is good for speed, but Google says it can "inadvertently hide content from Google" if done wrong. The reason is simple: Google Search does not interact with your page. Content that only loads after a scroll or a click may never appear.
Load content when it becomes visible in the viewport, using one of the methods Google names: the browser's built-in lazy loading for images and iframes, the IntersectionObserver API with a polyfill, or a library that loads data when it enters the viewport. Don't lazy-load content that is visible as soon as the page opens. To check it, look in the rendered HTML in URL Inspection: if your image or video URLs are in the src attribute of the <img> or <video> elements, it works. More on images in our image optimization guide.
For infinite scroll, Google asks for paginated loading:
- Give each chunk its own persistent, unique URL, for example with
?page=12, and avoid relative values such as?date=yesterday. - Show the same content at that URL every time it loads.
- Link to the pages in sequence so search engines can discover them.
- Update the displayed URL with the History API when a new chunk becomes the main thing on screen.
Server-side rendering and dynamic rendering
Client-side rendering means JavaScript builds the content in the browser. Google sees that content, within the limits above, but Google points out that other search engines "may choose to ignore JavaScript and won't see JavaScript-generated content".
Dynamic rendering was the old fix: the server detects crawlers by user agent and sends them a pre-rendered HTML version while users get the JavaScript version. Google now calls it "a workaround and not a long-term solution" and recommends server-side rendering, static rendering or hydration instead. Sites that still use it should know where Google draws the line: Googlebot generally doesn't treat dynamic rendering as cloaking as long as both versions carry similar content, but serving completely different content to users and crawlers "can be considered cloaking".
Testing a JavaScript page
Google's tools for this are the Rich Results Test and the URL Inspection tool in Search Console. Both show the loaded resources, JavaScript console output and errors, and the rendered DOM. Search the rendered HTML for the text, links and images you expect; if something isn't there, Google can't index it. Our URL Inspection guide explains the live test and the rendered view.
Google also suggests logging JavaScript errors that visitors (Googlebot included) run into, with a global error handler that sends them to your own server, and watching Googlebot's activity in the Crawl Stats report rather than in client-side analytics. Our Crawl Stats guide explains how to read it. After a fix, test the page again.
How we approach it
This is our own routine on the websites and web apps we build; the rules behind it are Google's.
- Pages meant to rank are served as complete HTML from the server or as static files. JavaScript adds behaviour on top.
- Screens behind a login stay out of the index; public pages get real paths, never
#/routes. - Every navigation item is an
<a href>, whatever the framework renders around it. - Missing records return a real 404 from the server, or redirect to one.
- Title, description and canonical are in the first HTML response, and no script changes the canonical.
- Built files carry a content hash in their names.
- Before launch, a sample of URLs goes through URL Inspection's live test, and we compare the rendered HTML with what a visitor sees.
Frequently asked questions
Can Google index a website built with JavaScript?
Yes. Google renders pages with an evergreen version of Chromium and indexes the rendered HTML. Rendering happens in a queue, though, and some features (permissions, storage between pages, WebSockets) don't work for Googlebot, so content that depends on them can be missed.
Does Google click buttons or scroll to load content?
No. Google says Search does not interact with your page. Lazy-loaded content must load when it is in the viewport, and infinite scroll needs separate paginated URLs.
Can I set the title and canonical tag with JavaScript?
You can set the title and meta description with JavaScript. For the canonical, Google prefers HTML; if you use JavaScript, keep it the same as the HTML canonical (or leave it out of the HTML) and make sure only one canonical tag exists.
Why does my single-page app show soft 404 errors?
Because the server returns 200 for every route, including missing items. Redirect those views to a URL that returns a real 404, or add a noindex robots meta tag to them with JavaScript.
Should I still use dynamic rendering?
Google calls dynamic rendering a workaround, not a long-term solution, and recommends server-side rendering, static rendering or hydration instead.
For more on technical SEO, see our guides to robots.txt and XML sitemaps, canonical tags, Core Web Vitals and progressive web apps.