SEO·AEO 给开发者

第 0006 课 · 你的爬虫漏掉了什么

JS 渲染缺口

你的页面有两个版本:服务器发出的 HTML,以及 JavaScript 运行之后的 DOM。一半的 bot 永远只看到第一个版本——而它们决定了你存不存在。

回顾 第 0002 课:你运行了 crawl_audit.py,它说“可抓取 + 可索引”。但那个工具是用 urllib 抓取的——它读取的是原始响应,从不运行一行 JS。那么它放行的那个页面,究竟含有任何东西吗?这一课就来找出这道缺口。

Google 并不是一次性就把你的页面索引(index)了。它先抓取(crawl)原始 HTML(raw HTML),然后——单独地、在之后、在它的资源允许时——一个 headless Chrome 会渲染(render)这个页面,运行 JavaScript,再把 JS 产出的东西重新索引。[1]渲染是一个延迟队列,不是抓取的一部分。如果你的内容只在 JS 之后才存在,那么在第二波到来之前它都是不可见的——而对很多 bot 来说,则是永远不可见。

你的收获: 在一个页面的原始 HTML 和它的渲染后的 DOM(rendered DOM)上运行 render_gap.py,得到一份逐信号的差异对比——词数、链接、JSON-LD、标题——精确标出哪些信号只在 JavaScript 之后才存在。那正是一个不跑 JS 的(no-JS)bot 永远看不到、而 Google 看得很晚的内容。

两波,不是一波

直接引自 Google 的 JavaScript-SEO 文档:页面是分阶段处理的,而渲染(render)位于它自己的队列里。[1]

Google 的流水线 · 两波
Crawl 第一波——抓取原始 HTML 并索引它。还没有 JS。
Render 延迟队列——headless Chrome 在资源允许时运行 JS
Re-index 第二波——JS 产出的内容、链接 + schema 被索引,前提是渲染成功了

▲ 渲染(Render)是它自己的延迟队列——可能要几秒,也可能更久得多。

Google 确实会“使用近期版本的 Chrome,以类似浏览器渲染页面的方式”运行你的 JavaScript。[2]问题在于什么时候,以及还有谁不运行

你的审计工具是个不跑 JS 的 bot——大多数 AI 爬虫也是

第 0002 课里的 crawl_audit.py,正是那种只看得到第一波的抓取器:

# crawl_audit.py — the fetch is plain HTTP. No browser, no JS.
req = urllib.request.Request(url, headers={"User-Agent": "Googlebot"})
body = urllib.request.urlopen(req).read()   # <- raw HTML only

所以如果你的 <title>、内容和 canonical 是 React 在加载之后注入的,crawl_audit.py 就看不到它们——而它可能仍然会对着一个近乎空壳的页面打出 PASS。同样的盲区也重重地打在 AEO 这一侧:独立的 AI 爬虫通常抓取原始 HTML(raw HTML)并且不执行 JavaScript,而且控制它们也没有定型的标准。[3]那些 Googlebot 最终会渲染的客户端内容,可能根本到不了答案引擎(answer engine)那里。

服务端渲染(SSR / SSG)——内容在第一个字节里

HTML 响应里已经含有文本、链接和 JSON-LD。JS 只是锦上添花。

第一波 Googlebot、不跑 JS 的 AI 爬虫、你自己的审计——所有人,都立刻看得到。

✓ 现在就被索引 · 现在就可被引用
客户端渲染(CSR)——空壳 + bundle

原始 HTML 就是 <div id="root"></div>。所有内容都通过 JS 送达。

对不跑 JS 的 bot 不可见;Googlebot 只在延迟渲染波时才索引它——前提是它渲染得干净。

✗ 对 Google 延迟 · 对 AEO 缺失

工具:对比两个版本

你给 render_gap.py 两样东西——原始 HTML(它可以自己抓,就像第 0002 课的工具那样)和渲染后的 DOM(rendered DOM)(这个由你来捕获:DevTools → Elements → 右键点 <html>Copy outerHTML)。它从两者中提取相同的信号,并标记出任何从 ≈0 跳到很多的信号:

# share of each signal present WITHOUT JS
visible = raw[key] / rendered[key]          # 1.0 = fully server-rendered
js_dep  = rendered[key] > 0 and visible < 0.5   # needs JS to exist
# headline keys on visible TEXT: >=90% server-rendered, <=10% client-rendered
现在就动手:
  1. 自检(离线):python3 tools/render_gap.py --demo
  2. 挑一个页面。把它渲染后的 DOM 存为 rendered.html(DevTools → Copy outerHTML),然后:python3 tools/render_gap.py https://yoursite.com/page rendered.html
  3. 用同样的方式测一个已知的 SPA(一个 React/Vue 应用的路由)——看着每个信号都被标成 JS-DEP。那就是一个不跑 JS 的爬虫看到的东西。
$ python3 tools/render_gap.py https://app.example.com/blog/geo rendered.html

Render gap — raw HTML (no JS)  vs  rendered DOM (after JS)
──────────────────────────────────────────────
[FAIL] visible words      no JS 0 · with JS 209 · visible 0% · JS-DEP
[FAIL] a links            no JS 0 · with JS 3   · visible 0% · JS-DEP
[FAIL] JSON-LD blocks     no JS 0 · with JS 1   · visible 0% · JS-DEP
[FAIL] h1 headings        no JS 0 · with JS 1   · visible 0% · JS-DEP
[FAIL] h2 headings        no JS 0 · with JS 1   · visible 0% · JS-DEP
──────────────────────────────────────────────
VERDICT: CLIENT-SIDE RENDERED — the raw HTML is an empty shell. No-JS AI crawlers see ~nothing; Googlebot indexes it only on the deferred render wave.
JSON-LD 陷阱:你在第 0003 课里那份完美的 schema 可能是不可见的。 如果你是通过 Google Tag Manager 或客户端 JS 注入结构化数据,那它存在于渲染后的 DOM 里——所以 schema_tool.py 能验证它,Googlebot 可能在第二波时拾取它,但一个不跑 JS 的 AI 爬虫一个属性都看不到。[3]客户端渲染的 <title> 和 canonical 也是同样的故事:crawl_audit.py 可以在原始响应是空壳的情况下放一个页面过关。通过抓取/索引这道关卡 ≠ 内容真的在那里。把事实放进服务器发出的 HTML 里。

要知道的上限:render_gap.py 不运行浏览器——渲染后的 DOM 由你提供,就像第 0005 课的追踪器拿的是你捕获的引擎输出一样。它的“visible words”统计的是文本节点,不是布局;一个完全由 JS 构建、Google 渲染起来毫无问题的页面,在这里仍然会被标记,而这正是重点——它展示的是不跑 JS 的视角,那是一个真实存在的受众,而不是对 Googlebot 的最终裁决。

提取练习 · 不许偷看

留意这道缺口

凭记忆作答——正是这份努力让知识留得住。每题只有一次机会;在看其他选项前先选。

第 1 / 4
在 Google 的流水线里,你的 JavaScript 究竟什么时候才真正运行?
第 2 / 4
为什么 crawl_audit.py 会对一个没有任何可见内容的页面打出 PASS?
第 3 / 4
为什么客户端渲染(CSR)对 AEO 比对传统 SEO 更糟糕?
第 4 / 4
某个信号从 0(原始)跳到很多(渲染后)。render_gap.py 把它叫作什么,修法是什么?
一手来源 — 接下来读这个 (≈10 分钟)
“Understand the JavaScript SEO basics” — Google Search Central

来自引擎自己的说法:三个阶段(crawl → render → index),以及为什么渲染(render)是 一个延迟队列。把它和 <a href="https://developers.google.com/search/docs/fundamentals/how-search-works">“How Search Works”</a> 里的渲染说明,以及 <a href="/zh/resources/"><code>RESOURCES.md</code></a> 里的 AI 爬虫控制缺口配合着看。

卡住了,或者好奇? 这个 agent 就是你的老师。尽管问——“给我看一个真实的 robots.txt”、“Claude 和 Perplexity 的 retrieve 方式不一样吗?”——追问是学得最快的方式。