JavaScript SEO 与渲染
Google 如何渲染 JavaScript,以及如何为 SEO 在 SSG、SSR、ISR 和动态渲染之间做出选择。
现代前端框架把大部分内容以 JavaScript 的形式交付,由浏览器在运行时执行。这对产品工程而言堪称美妙,但对那种「爬虫下载 HTML 并读取内容」的朴素 SEO 心智模型来说则是灾难。Google 确实会执行 JavaScript——但它按自己的节奏、用自己的预算来执行,而且存在一些会悄悄拖累你排名的失败模式。如果你最重要的内容只有在某个 fetch() 完成之后才存在,那你就是在拿自然流量去赌一次可能延迟数天、甚至永远不会按你预期完成的渲染。
本指南将解释 Google 渲染管线内部到底发生了什么,对比你可以在构建时选择的各种渲染策略,逐一剖析真实应用中常踩的坑,并给出一套你今天就能跑起来的调试流程。
Google 如何渲染
把索引过程想象成两轮处理(two-wave)而非一次性扫描,会更有帮助。
- 抓取(Crawl)。 Googlebot 请求 URL 并拿回初始 HTML——也就是
curl https://example.com会返回的内容。在这里发现的链接会进入抓取队列(crawl frontier)。如果你的初始 HTML 只是一个空的<div id="root"></div>,那么这一轮几乎什么都看不到。 - 渲染(Render)。 该 URL 被放入 Web 渲染服务(Web Rendering Service,WRS) 队列。WRS 是一个无头的、常青版(evergreen)Chromium。当有空余算力时,它会加载页面、执行 JavaScript、等待网络稳定,然后产出渲染后的 HTML(即执行完 JavaScript 之后的 DOM)。
- 索引(Index)。 Google 对渲染后的 HTML 建立索引,并提取其中发现的任何新链接,供下一轮抓取使用。
关键洞见在于第一轮和第二轮之间的间隔。渲染之所以被推迟,是因为执行 JavaScript 大约比抓取 HTML 昂贵一个数量级。实践中这个延迟通常是几分钟,但当抓取需求很高、或你的站点渲染队列很长时,它可能拉长到数天。
CRAWL RENDER (WRS queue) INDEX
Googlebot ──► raw HTML ──► [ wait: minutes → days ] ──► headless
│ Chromium
▼ │
links found rendered DOM
in raw HTML │
└──────────────► next crawl cycle ◄── new links found
这种推迟还会和两个你无法直接控制的预算相互作用:
- 抓取预算(Crawl budget)——在一段时间窗口内,Googlebot 从你的源站抓取多少个 URL。由站点权威度、服务器速度和错误率决定。
- 渲染预算(Render budget)——大致是指 WRS 愿意为渲染你的页面花费多少 CPU/时间。缓慢、脚本繁重的页面会消耗更多渲染预算,因此每一轮里被渲染的页面就更少。
⚠️ 注意:经典的失败场景是内容只在客户端注入之后才存在。抓取轮看到的是一个空壳,而如果渲染轮被延迟、或你的脚本在 WRS 下报错,内容就永远不会被索引。「在我的浏览器里没问题」并不能证明它对 Googlebot 也没问题——你的浏览器不会被限速、不会被沙箱隔离,也不会用着几周前的缓存响应在跑。
几个会让开发者意外的 WRS 细节:
- WRS 不会滚动、点击或移动鼠标。任何隐藏在用户交互背后的内容对它都是不可见的。
- WRS 会自动拒绝权限请求(地理位置、摄像头、通知)。任何在等待这些提示框的代码都会卡住。
- WRS 会按自己的节奏激进地缓存资源(JS、CSS、API 响应),缓存时间可能远比你的 HTTP 缓存头所暗示的要长。一次「全新部署」可能会针对陈旧的资源被渲染。
- WRS 使用当前版本的 Chromium,所以现代 JS 语法没问题——但你不能依赖
localStorage/sessionStorage在多次渲染之间持久化,cookie 也不会像已登录用户那样被携带。
渲染策略
你在哪里生成 HTML,决定了你要承担多少上述风险。下面是你实际会在其中做选择的五种策略。
| 策略 | HTML 何时构建 | 爬虫能否立即看到内容? | SEO 友好度 | 最适合 |
|---|---|---|---|---|
| CSR(客户端渲染) | 在浏览器里,JS 运行之后 | 否——先是空壳 | ⚠️ 有风险 | 登录后的仪表盘、鉴权之后的类应用 UI |
| SSR(服务端渲染) | 每次请求时,在服务器上 | 是 | ✅ 强 | 个性化或频繁变化的公开页面 |
| SSG(静态站点生成) | 在构建时 | 是 | ✅✅ 最强 | 文档、博客、营销页,以及任何稳定的内容 |
| ISR(增量静态再生成) | 在构建时,之后按计划/按需重新生成 | 是(提供上一份有效静态页) | ✅✅ 强 | 会变化但不需按请求变化的大型目录 |
| 动态渲染 | 每次请求时,机器人拿到预渲染 HTML,用户拿到 CSR | 是(对机器人而言) | ⚠️ 权宜之计 | 你无法重写的遗留 SPA |
表格承载不了的几点补充:
- CSR 是一个朴素的
create-react-app或未做配置的 SPA 的默认行为。它几乎是本指南中所有 JavaScript-SEO 问题的根源。在登录墙后面、没有任何需要索引的内容时,它完全没问题。 - SSR 在首次请求时发送完整成形的 HTML,然后将其「水合」(hydrate)成一个可交互的应用。爬虫在第一轮就拿到真实内容,所以渲染推迟不再重要。代价是每次请求的服务器算力和延迟。
- SSG 在构建时把每个页面预渲染成静态
.html文件。没有渲染需要等待,也没有服务器会变慢——爬虫读到的就是 CDN 提供的同一份文件。这是内容型站点的黄金标准。 - ISR 是可以刷新的 SSG:页面是静态的,但某个页面可以在 N 秒之后或按需在后台被重新生成(例如 Next.js 的
revalidate)。爬虫始终拿到一个有效的静态页面;新鲜度会略微滞后。当你有 50,000 个产品页、而每次价格变动都重建全部页面并不现实时,它是理想之选。 - 动态渲染会嗅探 user agent,向机器人提供一份预渲染快照(通过 Prerender.io 或 Puppeteer 之类的工具),而真实用户拿到的是 SPA。Google 如今称之为权宜之计,而非推荐做法——它增加了一层脆弱的 UA 检测,以及需要维护的第二条渲染管线。只有在重写应用完全不可行时才考虑它。
🧑💻 开发者视角:把它想成谁来支付渲染成本。CSR 让用户的设备和 Google 的 WRS 来支付(付两次)。SSR 让你的服务器在每次请求时支付。SSG 在构建时支付一次,此后再也不付。你越是把这笔成本往左推——推向构建时——你消耗的 Google 渲染预算就越少,渲染可能出错的方式也越少。
常见陷阱
下面这些是真实审计中会冒出来的问题,大致按照它们导致流量悄悄流失的频率排序。
1. 从不更新 <head> 的客户端路由。 在 SPA 中,在路由之间导航会替换 DOM,而不触发整页加载。如果你的路由器没有同时更新标题、meta 描述和 canonical,那么每个路由都会继承初始 HTML 里的那一份。结果是 Google 索引了十个页面,而它们都声称自己是你的首页。
// ❌ 每次软导航后标题和 canonical 都是过时的
router.push('/pricing'); // DOM 变了,<head> 没变
// ✅ 每次路由变化时更新 head 元数据
router.afterEach((to) => {
document.title = to.meta.title;
document
.querySelector('link[rel="canonical"]')
.setAttribute('href', `https://example.com${to.path}`);
});
2. 需要滚动或交互才能加载的延迟加载内容。 无限滚动、「加载更多」按钮,以及通过 IntersectionObserver 挂载的内容,对 WRS 都是不可见的,因为它不会滚动或点击。如果你的文章正文或产品列表只有在滚动事件之后才出现,那它就不在 Google 索引的渲染后 DOM 里。
3. 基于哈希(hash)的路由。 形如 example.com/#/products/42 的 URL 是老旧 SPA 的遗物。# 之后的一切都是片段(fragment)——按规范,它永远不会被发送给服务器,而 Google 会把它当作与 example.com/ 同一个 URL。哈希路由会把你整个站点坍缩成一个可索引页面。请始终使用 History API(pushState),让路由成为真实的、服务器可解析的路径。
4. 超时与阻塞渲染的工作。 WRS 会放弃耗时过长的渲染。一连串相互依赖的 fetch() 调用、一个 4 MB 的打包文件,或一个挂起的第三方脚本,都可能把内容推到截止点之外。缓慢的渲染还会烧掉渲染预算,于是你被渲染的页面总数就更少了。
5. 隐藏在用户交互或同意背后的内容。 一道在点「接受」之前阻塞渲染的 cookie 同意墙、内容只在点击后才加载的标签页,或藏在「阅读更多」按钮后面的文案——WRS 一个都不会点。如果它对 SEO 很重要,那它就必须在没有交互的情况下存在于 DOM 中。
6. 软 404 和由 JS 驱动的错误状态。 当 SPA 找不到某个资源时,它常常会渲染一条「未找到」消息,但仍然返回 HTTP 200。Google 看到的是一个成功响应外加单薄的内容,可能会把这个错误页给索引了。请从服务器返回真正的 404/410 状态,或对确实缺失的内容使用 noindex。
最佳实践
贯穿始终的主线是:把关键内容和元数据放进那份在 JavaScript 运行之前就已存在的 HTML 里,并把 JavaScript 当作增强手段来对待。
- 在服务端或构建时渲染关键内容。 标题、正文、主导航、产品详情和价格,都应该通过 SSR 或 SSG 进入初始 HTML。让 JavaScript 去增强一个已经完整的页面,而不是去创造它。
- 践行渐进增强(progressive enhancement)。 在禁用 JS 的情况下,页面也应该传达出它的核心含义。链接应该是真正的
<a href>元素(WRS 跟踪<a href>,而不是调用router.push的onClick处理函数)。用于导航的按钮应该改成链接。 - 使用 History API,并按路由更新元数据。 每个路由都需要自己的
title、meta 描述和自引用的canonical,并在导航时更新。一个框架级的 head 管理器(react-helmet-async、@unhead/vue、Next/Astro 的内置方案)能干净利落地处理这件事。
// 与框架无关的按路由 head 设置
function setSeoHead({ title, description, canonical }) {
document.title = title;
upsertMeta('description', description);
upsertLink('canonical', canonical);
}
- 把结构化数据放进 HTML,而不是藏在延迟的 fetch 后面。 JSON-LD 应当尽早出现在渲染后的 DOM 里。SSG/SSR 会直接输出它;如果你不得不在客户端注入,请在挂载时同步完成,绝不要在一次异步数据往返之后才注入。
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "JavaScript SEO & Rendering",
"datePublished": "2026-06-22"
}
</script>
- 让 JS 保持精简且有韧性。 做代码分割、推迟非关键脚本,并确保单个第三方失败不会把页面整张抹白。自托管分析和标签管理脚本,或给它们加上
async,让它们永远不会阻塞内容。 - 不要在
robots.txt里屏蔽 JS/CSS。 WRS 需要抓取你的脚本和样式才能渲染页面。屏蔽/_next/或/assets/意味着 Google 渲染出来的是一个坏掉的页面。
💡 提示:对任何页面都可以做一次快速的合理性检查——运行
curl -s https://your-url | grep "your headline text"。如果你的主要内容不在这份输出里,那么抓取轮就看不到它,你完全是在依赖那个被推迟的渲染轮。这正是 SSG/SSR(内容存在)和 CSR(内容缺失)之间的分水岭。
调试
你不必去猜 Google 看到了什么——它的工具会直接把渲染输出展示给你。
- Google Search Console 中的网址检查(URL Inspection)。 检查一个实时 URL,然后打开 测试实际网页(View Tested Page)→ HTML。这就是 WRS 产出的渲染后的 HTML。在里面搜索你的标题、你的 canonical、你的 JSON-LD。如果它们在这里缺失,那它们在索引里也缺失。屏幕截图(Screenshot) 和 更多信息(More info)→ 网页资源(Page resources) 标签页会暴露出那些破坏了渲染的被屏蔽脚本和控制台错误。
site:运算符。site:example.com/pricing或site:example.com "页面里的精确短语"会告诉你某个页面(以及特定的、由 JS 注入的文本)是否真的被索引了。如果某条只存在于你渲染后 DOM 里的短语搜不到结果,这就是渲染没有落地的强烈信号。- 自己对比原始 DOM 与渲染后 DOM。
view-source:(或curl)显示的是抓取轮的 HTML。DevTools → Elements 显示的是执行 JS 之后的 DOM。两者之间的差异恰恰就是依赖一次成功渲染才能呈现的内容——对任何 SEO 关键内容,都要把这个差异最小化。 - 模拟 Googlebot 的约束条件。 在 Chrome DevTools 中禁用 JavaScript(命令菜单 → 「Disable JavaScript」)并重新加载:这近似于抓取轮。对网络和 CPU 进行限速,可以把那些会在 WRS 负载下超时的渲染暴露出来。免费的 富媒体搜索结果测试(Rich Results Test) 和 移动设备适合性测试(Mobile-Friendly Test) 也会用 Google 自己的引擎来渲染,并报告渲染错误。
⚠️ 注意:在验证一处修复时,永远去测试实时 URL,而不是 Google 的缓存版本——缓存反映的是上一次成功的渲染,它可能比你的部署还早好几天。
从这里出发
JavaScript 渲染本质上是你在构建层做出的一个决定——你选的框架和输出模式决定了你究竟会不会碰上这些问题。深入了解这些权衡,可阅读构建层指南。
本站本身就通过使用 Astro 加静态生成绕开了一整类问题:每个页面都在构建时被预渲染成 HTML,所以抓取轮看到的是完整的内容、结构化数据和元数据,完全不依赖那个被推迟的渲染队列。这并非偶然——它是当下可选的最 SEO 友好的默认方案,也正是内容型站点应当首选 SSG 的原因。
关键要点
- ✅ Google 会在一个被推迟的第二轮(WRS)里渲染 JavaScript;永远不要假设注入的内容会被及时——甚至根本——索引。
- ✅ 把渲染成本往构建时推:SSG 最强,SSR 强,CSR 对任何你想被索引的内容都有风险。
- ✅ 在每一次客户端路由变化时更新
title、meta 描述和自引用的canonical。 - ✅ 把关键内容和 JSON-LD 放进那份在 JS 运行之前就存在的 HTML 里;WRS 不会滚动、点击或授予权限。
- ✅ 用 GSC 网址检查(渲染后的 HTML)、
site:运算符以及禁用 JS 重新加载来验证——测试实时 URL,而不是缓存。