Google Search is the only mainstream search engine that runs JavaScript by default (using evergreen Chromium). Others (Bing has partial support, DuckDuckGo / Yandex / most niche engines)do not run JS — SSR remains the safest choice.
Core mechanism of how Google processes JS sites:WRS (Web Rendering Service) + render queue.
1. Google's 3-stage JS processing + render queue
URL
↓
[crawl queue] → Googlebot 取 URL → 检查 robots.txt
↓
fetch HTML → parse → 抽取 link 回 crawl queue
↓
[render queue] → WRS (headless Chromium) 跑 JS → 渲染后 HTML
↓
parse 渲染后 HTML → 抽 link 回 crawl queue + 用于 indexing
↓
[index]
Key invariants:
- All HTTP
200pages enter the render queue,regardless of whether they contain JS(Google makes the judgment) - non-200 statuses (e.g. 404) may skip rendering
- Rendering may wait from a few seconds to longer — determined by Google's resources
- WRS uses the current evergreen Chromium, synced with the latest Chrome
- Resources blocked by robots.txt → will not be fetched → missing resources for rendering → Google may not see the content
Counterintuitive: the render queue is asynchronous. If you change the page via hash but the render queue is congested, what Google sees may be a snapshot from several days ago.
2. 15 JS SEO Best Practices (must-read)
Ranked by importance.
2.1 Content-related
| # | Practice | Key points |
|---|---|---|
| 1 | Still use SSR / pre-rendering | Faster for both users and crawlers,non-Google crawlers can also see content |
| 2 | unique descriptive <title> + meta description |
JS setup is OK, but ensure these two exist when rendering completes |
| 3 | Use <a href> instead of onclick |
Google only extracts links from <a href>Google only extracts links from <a href> |
| 4 | History API instead of URL fragments | #/products This kind of fragment routing is unreliable for Googlebot (the AJAX crawl scheme was deprecated in 2015) |
| 5 | Add structured data use JSON-LD, JS injection is OK | See Complete Overview of Structured Data (Frameworks + One-Click Lookup for 34 Rich Result Types + JSON-LD/Microdata/RDFa Selection) |
| 6 | For Web Components use <slot> to display light DOM |
Otherwise light DOM content will not enter the rendered HTML → will not be indexed |
| 7 | For lazy loading use viewport detection, not user scroll | Google Google does not scroll your page |
2.2 Error handling
| # | Practice | Key points |
|---|---|---|
| 8 | Use accurate HTTP status codes | 404 for not found / 401 for login wall / 301 for moved |
| 9 | Avoid soft 404s in SPAs | Client-side routing often returns 200 for not-found → fake 200 pages get indexed. Instead use JS redirect to a real 404 URL or inject <meta name="robots" content="noindex"> |
| 10 | Be careful when injecting robots meta tags via JS | If Google sees noindex it may skip rendering → using JS to remove noindex is unreliable;If you want to be indexed, do not include noindex in the original HTML |
2.3 Performance / caching
| # | Practice | Key points |
|---|---|---|
| 11 | Long-lived cache + content fingerprinting | WRS may ignore cache headers; use main.2bb85551.js hash filenames like this to avoid issues |
| 12 | feature detection + polyfill | Detect unsupported APIs and provide fallbacks; for some (such as WebGL) Google does not support them, so SSR that portion |
| 13 | Use HTTP, do not rely on WebSockets/WebRTC | Googlebot Only HTTP is sent, other connection types cannot be used |
2.4 Canonical / Robots
| # | Practice | Key points |
|---|---|---|
| 14 | rel="canonical" should be placed in HTML first |
JS injection is acceptable if absolutely necessary, but only one can be retained canonical; do not use JS to change it to a value different from the original HTML |
| 15 | Do not force permissions such as Camera/Geo/Notification | Googlebot rejects permission requests; provide a path that works without permissions path |
3. 6 Major SPA Pitfalls (single-page application specific)
| Pitfall | Symptom | Fix |
|---|---|---|
| soft 404 | Client-side routing returns 200 for not-found → fake error pages get indexed |
JS redirect to a real 404 URL, or inject <meta robots="noindex"> |
Fragment routing (#/products) |
Googlebot cannot crawl content after the fragment | History API: window.history.pushState({}, '', '/products') |
| Local Storage / Session Storage failure | Data lost across page loads | WRS Local/Session Storage and Cookies are cleared on every page load.Do not rely on persistent data to serve content |
| Fetching data via WebSocket / WebRTC | Googlebot cannot retrieve it | Provide an HTTP fallback |
| Modifying canonical via JS | Multiple conflicting canonicals → unpredictable behavior | There can be only one canonical per page, check before JS injection |
| Fake JS paywall implementation | Hiding full text in HTML + hiding it via JS | Actual access restriction must be done server-side. Hiding via JS is just for show |
4. 11-Step Troubleshooting Checklist
When you suspect JS is blocking indexing:
- Run Rich Results Test / URL Inspection Tool — check loaded resources / JS console / exceptions / rendered DOM. Use Search Console's Crawl Stats Report to monitor Googlebot + WRS activity
- Prevent soft 404s — see §3 item 1
- Expect Googlebot to reject permission requests — do not force Camera/Geo/Notification
- Do not use URL fragments to load different content — use the History API
- Do not rely on data persistence — WRS clears Local/Session/Cookie every time
- content fingerprinting —
main.<hash>.jsto prevent stale cached resources - Feature detection + fallbacks / polyfills for critical APIs
- HTTP connections are required — do not use WebSocket / WebRTC to fetch main content
- Web Components render correctly —
<slot>Is used? Use Rich Results Test to check if rendered HTML content is complete - JS paywall implementation — full content should not be in HTML
- Test again after fixing — re-verify with Rich Results Test / URL Inspection. Green check means pass
Collect production JS errors: It is recommended to add a global onerror handler to log JS errors from both Googlebot and real users remotely, to locate render failure causes.
window.addEventListener('error', function(e) {
const errorText = [
e.message,
'URL: ' + e.filename,
'Line: ' + e.lineno + ', Column: ' + e.colno,
'Stack: ' + (e.error && e.error.stack || '(no stack trace)')
].join('\n');
// POST 到自己服务器
fetch('/logError', { method: 'POST', body: errorText });
});
5. Dynamic Rendering is a Historical Workaround (not recommended in 2026)
Conclusion: do not use dynamic rendering. It has been deprecated as a recommended solution; use SSR / static rendering / hydration instead.
5.1 What it does
请求 → 检测 user-agent
├─ Googlebot / 不支持 JS 的 crawler → 路由到 rendering server → 返回静态 HTML
└─ 普通浏览器 → 返回 client-rendered 版本
5.2 Why it is deprecated
- Increases server complexity (dual-track serving)
- High maintenance cost
- SSR / hydration solve the same problem and are simpler
- The real "crawler list" is hard to maintain; new bots appear that you have not adapted for
5.3 Conditions under which it does not count as cloaking
As long as the content dynamic rendering serves to both sides is similar(server-rendered version ≈ client-rendered version), Googlebot does not treat it as cloaking.
Real cloaking(violating Google SEO Basics + How Search Works + Search Essentials §7 spam policy) example: showing cats to users, showing dogs to crawlers.
5.4 Modern alternatives
| Solution | Applicable scenario |
|---|---|
| SSR (Server-Side Rendering) | Default recommendation. Next.js / Nuxt / Astro etc. |
| Static rendering | Content does not change often → generate HTML at build time |
| Hydration | SSR + subsequent client-side takeover of interactivity |
| Pre-rendering(SSR-like) | Convenient for small sites |
6. Correct Lazy Loading Practices
6.1 Core principle
Lazy loading must use viewport detection, not scroll triggering. Google does not scroll your page.
6.2 3 implementation methods
| Method | When to use |
|---|---|
Browser built-in loading="lazy"(<img> / <iframe>) |
Natively supported by modern browsers, simplest |
| IntersectionObserver API + polyfill | Custom logic, video lazy loading |
| JS libraries (react-lazyload etc.) | Framework integration |
6.3 What not to do
- Do not apply lazy loading to above-the-fold visible content (slow and pointless)
- Do not rely on scroll events to trigger (Google does not scroll)
6.4 Correct implementation of infinite scroll
Infinite scroll is naturally contradictory to SEO; you need paginated:
- one persistent unique URL per chunk(
/articles?page=12) - The same URL loads consistent content every time(avoid
?date=yesterdayrelative parameters like this) - Sequentially link each page URL (so crawlers can discover them)
- When the user scrolls to a new chunk,update the URL via History API(so users can refresh/share/link)
- See Complete Ecommerce SEO Guide (6 Surfaces / 4 Launch Strategies / Merchant Center / 6 SD Types / 3 Pagination Modes / 13 Review Guidelines) pagination chapter
6.5 Verification
Search Console URL Inspection Tool → Live URL → check rendered HTML
<img src>/<video src>Real URLs are all present → success- All placeholder URLs → failure, Google did not see the real images
7. Key Cheat Sheet
| What you want to do | How to do it | Warnings |
|---|---|---|
| Set page title | JS setup OK | Ensure the title exists when rendering completes |
| Add structured data | Inject JSON-LD via JS | See Complete Overview of Structured Data (Frameworks + One-Click Lookup for 34 Rich Result Types + JSON-LD/Microdata/RDFa Selection) |
| Set canonical | HTML preferred; JS injection is OK | Only one per page, do not create conflicts |
| Set robots meta | HTML preferred | noindex may skip rendering → JS cannot modify it |
| Routing (SPA) | History API | Do not URL fragment(#/foo) |
| Handle 404 | JS redirect to the real 404 URL, or inject noindex | Client-side fake 200 responses will be indexed |
| Add image lazy loading | viewport detection | Do not trigger on scroll |
| Implement infinite scroll | Paginated URL + History API updates | Each chunk has an independent persistent URL |
| Detect API support | feature detection + polyfill | Google does not support WebGL/WebSocket and similar technologies |
| Fetch data | HTTP fetch | Do not pull main content via WebSocket/WebRTC |
| Cross-page persistence | Server side | WRS clears Local/Session/Cookie every time |
| Add cache control | content fingerprinting | WRS may ignore cache headers |
| Web Components | <slot> display light DOM |
Shadow DOM only content is not indexed |
| Paywall | Server controls full content | Hiding via JS does not count as restriction |
8. Relationship with other wikis
Sources and related materials
- https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics?hl=en
- https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics?hl=zh-cn
- https://developers.google.com/search/docs/crawling-indexing/javascript/dynamic-rendering?hl=en
- https://developers.google.com/search/docs/crawling-indexing/javascript/dynamic-rendering?hl=zh-cn
- https://developers.google.com/search/docs/crawling-indexing/javascript/fix-search-javascript?hl=en
- https://developers.google.com/search/docs/crawling-indexing/javascript/fix-search-javascript?hl=zh-cn
- https://developers.google.com/search/docs/crawling-indexing/javascript/lazy-loading?hl=en
- https://developers.google.com/search/docs/crawling-indexing/javascript/lazy-loading?hl=zh-cn
- Google SEO Basics + How Search Works + Search Essentials
- Complete Guide to Googlebot Crawling Mechanism and Indexing Control (URL/Links/Mobile-first/Meta/Testing/AMP)
- Complete Guide to Canonical / Redirects / Site Migration (Including Full HTTP Status Code Table + 503 Temporary Outage Playbook)
- Complete Guide to Crawling and Indexing Control (robots.txt / noindex / X-Robots-Tag / data-nosnippet / rel)