🧩

JavaScript SEO 与渲染

Google 如何渲染 JavaScript,以及如何为 SEO 在 SSG、SSR、ISR 和动态渲染之间做出选择。

📖 12 分钟阅读 🕑 更新于 2026-06-22

现代前端框架把大部分内容以 JavaScript 的形式交付,由浏览器在运行时执行。这对产品工程而言堪称美妙,但对那种「爬虫下载 HTML 并读取内容」的朴素 SEO 心智模型来说则是灾难。Google 确实会执行 JavaScript——但它按自己的节奏、用自己的预算来执行,而且存在一些会悄悄拖累你排名的失败模式。如果你最重要的内容只有在某个 fetch() 完成之后才存在,那你就是在拿自然流量去赌一次可能延迟数天、甚至永远不会按你预期完成的渲染。

本指南将解释 Google 渲染管线内部到底发生了什么,对比你可以在构建时选择的各种渲染策略,逐一剖析真实应用中常踩的坑,并给出一套你今天就能跑起来的调试流程。

Google 如何渲染

把索引过程想象成两轮处理(two-wave)而非一次性扫描,会更有帮助。

  1. 抓取(Crawl)。 Googlebot 请求 URL 并拿回初始 HTML——也就是 curl https://example.com 会返回的内容。在这里发现的链接会进入抓取队列(crawl frontier)。如果你的初始 HTML 只是一个空的 <div id="root"></div>,那么这一轮几乎什么都看不到。
  2. 渲染(Render)。 该 URL 被放入 Web 渲染服务(Web Rendering Service,WRS) 队列。WRS 是一个无头的、常青版(evergreen)Chromium。当有空余算力时,它会加载页面、执行 JavaScript、等待网络稳定,然后产出渲染后的 HTML(即执行完 JavaScript 之后的 DOM)。
  3. 索引(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.pushonClick 处理函数)。用于导航的按钮应该改成链接。
  • 使用 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/pricingsite: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,而不是缓存。