你上线了一个 React/Vue/Svelte SPA。URL 在 Google 索引里。但搜页面正文里一段独特的话,啥都搜不到。打开 Search Console,跑 URL Inspection,点 Test live URL,再点 View tested page,再切到 HTML 标签页。body 几乎是空的,或者只有一个 loading spinner。Googlebot 拿到了壳,没拿到内容。同样的问题还会表现成”已索引但零曝光”、页面什么词都排不上、或者明明页面上看得见的内容却过不了 rich result 校验。
最快的修法:只要内容对搜索重要,就在服务端渲染(SSR)或预先构建(SSG),让它出现在原始 HTML 里,而不是事后靠 JavaScript 画出来。下面的内容是在你没法立刻上 SSR/SSG 时,用来诊断渲染轮为什么失败的。
Googlebot 到底怎么渲染 JavaScript(截至 2026 年 6 月)
Google 用两轮来索引 JS 页面:
- 抓取(Crawl)——Googlebot 取回原始 HTML,把这份响应里已经有的文字、链接、metadata 直接索引。
- 渲染(Render)——页面被排进 Web Rendering Service(WRS)的队列,那是一个常青版(evergreen)的无头 Chromium。它执行你的 JavaScript,等网络静下来,再重新读一遍生成出来的 DOM。
有两件事决定你的内容能不能在这一轮里活下来:
- 不存在硬性的”5 秒渲染上限”。 这个数字是个误传,来自一个旧的中位数统计。Google 的 Martin Splitt 说过,约 99% 的页面在几分钟内就渲染完,而那个常被引用的 5 秒是抓取到渲染的中位间隔,不是每个页面的截止时间。所以”再多等等”几乎从来都不是真正的解法。内容缺失意味着渲染失败了或者根本没触发,不是”超时了”。
- WRS 不会跟页面交互。 它不点击、不滚动、不悬停、不切换标签页,也不会闲到足以触发懒加载逻辑。按 Google 自己的 JavaScript SEO 文档,只在用户操作后才出现的内容它看不到。这是下面最常见的一个原因。
常见原因
1. 内容只在用户操作之后才拉
组件看起来是挂载时拉数据 useEffect(() => fetch('/api/article')),但实际上是在 requestIdleCallback、scroll handler、IntersectionObserver 或点击之后才触发。Googlebot 从不触发这些。
怎么判断:在 Chrome DevTools 里打开页面,在 Console 里快速确认没有 scroll 或 idle 回调在卡住你的 fetch。或者把 Network throttling 调到 “Slow 4G”,啥都别动等 5 秒。如果 body 不会自己画出来,Googlebot 看到的也是同样的空白。
2. 渲染依赖的 API 被 robots.txt 挡了
HTML 加载了,JS 调 /api/data。但 /api/* 在 robots.txt 的 Disallow: 下面。Google 文档说得很明确:“Google Search won’t render JavaScript from blocked files or on blocked pages。” 拿不到 API 响应,就没有渲染出来的内容。
怎么判断:打开 https://yoursite.com/robots.txt,确认页面渲染依赖的数据端点没被 disallow。在 URL Inspection 里,View tested page → More info → Page resources 会列出 Googlebot 尝试的每个 fetch,并标出加载失败的那些。
3. JavaScript 报错把渲染搞挂
一个像 Cannot read properties of undefined (reading 'map') 的运行时错误让 hydration 中断。壳画出来了,数据层没有。WRS 不会重试你的应用;一个没接住的 throw,内容就没了。
怎么判断:在 URL Inspection 里跑 live test,然后 View tested page → More info → JavaScript console messages。(这个面板和渲染截图只在 live test 里有,已索引的快照里没有。)那里任何一条 error 就是你的渲染挂掉的地方。
4. 渲染太重或太慢,没跑完
WRS 不是无限耐心的。阻塞渲染的第三方脚本、几兆大的 bundle、或者一串慢 API 调用,都可能让 WRS 抓取 DOM 时内容还没画出来。这是资源问题,不是固定时钟。
怎么判断:跑 Lighthouse。LCP 超过约 4 秒、或 Total Blocking Time 超过约 600ms,说明关键内容出现得太慢、有风险。把 URL Inspection 里的渲染截图和你的线上页面对比一下。
5. 客户端路由从不更新 title 和 meta
用户通过 SPA 路由访问 /article/foo。title 和 meta 还停在首页的值,因为路由从没更新它们。Googlebot 索引第二个 URL 时拿的是首页 metadata,于是这个页面只会”排”在首页的主题上。
怎么判断:连点 3 篇文章,每次切换后看 DevTools Elements 面板里的 title。如果不变,说明路由没更新 document.title。
6. Service worker 给 Googlebot 发了旧内容
PWA service worker 缓存了一个跟当前 API 不兼容的旧 bundle。登录用户走 cache-bust 拿到新副本,Googlebot 走普通 fetch 撞上坏掉的缓存版本。
怎么判断:在 DevTools 里打开 Application → Service Workers,点 “Unregister”,然后硬刷新,看没缓存的渲染结果。再把 User-Agent 设成 Googlebot/2.1 的请求和普通浏览器请求对比;HTML 不一样就说明 service worker 或 CDN 在分流。
7. 纯 CSR,没有给爬虫的 fallback
HTML 就是 <div id="root"></div> 加一个 script 标签。渲染轮是看到内容的唯一机会,所以上面 1 到 4 任一出问题,索引里就是永久空白。
怎么判断:跑 curl https://yoursite.com/article/foo。如果不管 URL 是啥都返回同一个壳,就说明没有预渲染 HTML、没有 SSR——每个页面都全押在一次零失误的渲染上。
你属于哪一类?
| URL Inspection 里的症状 | 最可能的原因 | 看哪一步 |
|---|---|---|
| Rendered HTML 有壳但没有正文 | fetch 卡在交互上(1)或 JS 报错(3) | 第 1、3 步 |
Page resources 显示某个 /api/... fetch 被挡或失败 | robots.txt 或 CORS 挡了 API(2) | 第 4 步 |
JavaScript console messages 有运行时错误 | 渲染崩了(3) | 第 1、3 步 |
| 渲染截图是空白/spinner,Lighthouse LCP 高 | 渲染太慢/太重(4) | 第 3 步 |
正文索引正常,但每个 URL 的 title/meta 是错的 | 路由没更新 head(5) | 第 5 步 |
curl 对每个 URL 返回同一个壳 | 纯 CSR,没有 fallback(7) | 第 2 步 |
最短修复路径
第 1 步:先确认 Googlebot 到底渲染出了什么
Search Console → URL Inspection → Test live URL → View tested page → HTML。这个标签页就是 Googlebot 跑完你的 JavaScript 之后的 DOM——不是 curl 拿到的,也不是浏览器看到的。同一个 live test 里再打开 More info → JavaScript console messages 和 Page resources;它们只在 live test 里有数据,会立刻告诉你失败到底是崩溃、是资源被挡、还是内容根本没触发。
第 2 步:上服务端渲染或预渲染
最稳的修法是别再让需要被索引的内容靠客户端渲染:
- SSR——Next.js
getServerSideProps或 React Server Components、NuxtuseAsyncData、SvelteKit+page.server.ts。 - SSG——Astro、Next.js
generateStaticParams/ 静态导出、Hugo。适合不随每次请求变化的内容。 - Prerender——Prerender.io,或自己托管一个 Rendertron 快照发给爬虫。注意 Google 现在把 dynamic rendering 称为 “a workaround and not a long-term solution”,所以把它当成通往 SSR/SSG 的过渡,而不是终点。
// Next.js(Pages Router):文章页 SSR
export async function getServerSideProps({ params }) {
const res = await fetch(`https://api.example.com/articles/${params.slug}`);
const article = await res.json();
return { props: { article } };
}
第 3 步:关键内容不靠交互就能渲染
把数据拉取从 requestIdleCallback、scroll handler、IntersectionObserver 里挪出来。标题、正文、结构化数据必须在服务端响应里,或者在加载时就跑的、无门槛的 useEffect/onMounted 里拉。给渲染加好兜底,让 fetch 失败时显示真实文字而不是永久 spinner;用可选链(optional chaining)包住数据访问,这样一个缺失字段不会 throw 把整个渲染搞挂。
第 4 步:放行渲染依赖的 API 端点
如果客户端渲染没法避免,就别 Disallow: 页面读取数据的那些端点:
User-agent: Googlebot
Allow: /api/articles/
Allow: /api/categories/
Disallow: /api/admin/
同源 API 最省事;如果 API 在另一个域名上,确认它对这次 fetch 返回了宽松的 CORS 头,因为 CORS 失败在渲染结果里看起来跟资源被挡一模一样。
第 5 步:每次客户端路由切换都更新 title 和 meta
继续做纯客户端 SPA 的话,用 React Helmet / Next Head、Vue 的 useHead(@unhead/vue),或者在每次路由切换时直接更新 document.title 和 meta:
router.afterEach((to) => {
document.title = to.meta.title || 'Site Name';
document
.querySelector('meta[name="description"]')
?.setAttribute('content', to.meta.description || '');
});
第 6 步:批量测渲染,别一个 URL 一个 URL 地看
只看一次 URL Inspection,发现不了在 30% 页面上才出问题的模式。用能渲染 JS 的爬虫——Screaming Frog 开 JavaScript 渲染、Sitebulb、或者一个 Puppeteer 脚本——爬 100+ 个 URL,把 rendered HTML 和原始 HTML 做 diff。差异大的(大量内容只在渲染后才出现的)就是有风险的页面。
第 7 步:确认修好了,再盯 Search Console
确认方法:重跑 URL Inspection 的 live test,确认 HTML 标签页现在含有你的正文和正确的 title。如果上了 SSR/SSG,再 curl 一下这个 URL,确认内容在原始响应里。然后 request indexing。修完之后给 1 到 3 周,“Crawled - currently not indexed” 的数量回落、正文 query 的曝光开始出现,具体多久取决于 Google 多久重爬一次这个版块。
容易误判的情况
误以为是内容质量问题。页面看起来按 URL 被索引了,于是很容易想去重写文章。但如果正文从没进索引,Google 根本不知道这页讲啥,那么文笔再好它也什么词都排不上。动内容之前,永远先看 rendered HTML。
哪些情况不是你代码的锅
有些老的框架配置对 Googlebot 渲染本来就有已知坑(老版本 Angular Universal、老 Ember)。真正的修法是升级框架,而不是绕着补。另外,按 Martin Splitt 2026 年的说法,Google 的 AI 界面(Gemini 和 AI Overviews)现在用同一套 WRS 流水线渲染 JavaScript,所以一个对经典 Search 隐形的 CSR 页面,对这些界面同样隐形——把渲染修好,两边一起好。
预防建议
- 任何想拿排名的内容,默认 SSR 或 SSG。
- 永远别
Disallow:渲染依赖的 API。 - 每月对抽样 URL 跑一次 rendered-vs-原始 HTML 的 diff。
- 给 JS bundle 大小和渲染时间定预算,CI 超阈值就 fail。
- SPA 路由切换一定要更新
document.title和 meta。
FAQ
- Google 现在不是能完整渲染 JS 了吗? 它能渲染大多数 JavaScript、大多数时候、最终能渲染——但渲染轮是延后的、跑在预算上、不跟页面交互、还会跳过崩溃的内容。“会渲染 JS”不等于”任何情况都稳定渲染”。
- 真的没有 5 秒超时? 没有固定的每页截止时间。被反复引用的”5 秒”是抓取到渲染的中位间隔;Google 说过大多数页面在几分钟内渲染完。内容缺失意味着渲染失败或根本没触发,不是”时间用完了”。
- 预渲染会因为 cloaking 被惩罚吗? 不会,只要你发给爬虫的 HTML 和用户看到的一致。cloaking 是给爬虫看不同的内容;用不同的技术路径给出相同的内容是没问题的。
- 为什么正文索引了但 title 是错的? 你的 SPA 路由切换时更新了内容,却没更新
document.title/meta。看第 5 步。 - Googlebot 抓了我的页面,但 rendered HTML 还是空的,怎么办? 跑 live test,读 JavaScript console messages 和 Page resources。有 console error 说明渲染崩了(第 3 步);某个
/api/...fetch 被挡或失败说明是 robots.txt 或 CORS(第 4 步)。
相关阅读
- JavaScript 动态设置的标题未被 Google 索引
- 无限滚动页面不被索引
- Crawled, currently not indexed 怎么解
- 页面被索引但零曝光
- Discovered, currently not indexed 怎么解
- robots.txt 挡了 CSS/JS 拖垮索引
- Google 没发现你的站内链接
- 页面被索引但排不上
- Sitemap 提交了但 URL 还是没被索引
- 内容深度不足问题
标签: #SEO #排查 #收录 #Search Console #javascript #spa #rendering