JavaScript SEO 常见问题与最佳实践
JavaScript 已无处不在,多数网站都会用它增强交互。但网站上的大部分 JavaScript 并不影响 SEO,真正需要掌握的,是 Google 如何处理 JavaScript,并如何排查由此引发的问题。
Ahrefs 博客基于 WordPress 搭建,但站点其余的大部分内容由 React 之类的 JavaScript 驱动。
JavaScript 已无处不在。多数网站都会使用某种 JavaScript 来增强交互、改善用户体验。
不过,网站上的大部分 JavaScript 并不会影响 SEO。如果使用的是没有太多定制的常规 WordPress 站点,下面这些问题大多与之无关。
这里并不是要求 SEO 从业者去学习编写 JavaScript,恰恰相反,多数人并不需要接触这些代码。SEO 真正需要了解的,是 Google 如何处理 JavaScript,并如何排查由此引发的问题。
什么是 JavaScript SEO?
JavaScript SEO 属于技术 SEO(搜索引擎优化)的范畴,目标是让重度依赖 JavaScript 的网站易于抓取、易于索引,并对搜索友好,进而被搜索引擎发现并获得更高的排名。
JavaScript 对 SEO 而言谈不上有害。它只是与许多 SEO 从业者熟悉的形态不同,存在一定的学习门槛。
许多流程与既有的 SEO 做法相似,差别往往在细节。多数时候,你查看的仍是 HTML 代码,而无需真正研究 JavaScript。
所有常规的站内 SEO 最佳实践依旧适用,可参阅我们的站内 SEO 指南。
若所用框架没有内置相应能力,也能找到类似插件的方案来处理大量基础 SEO 元素。在 JavaScript 框架中,这类方案被称为模块(modules),可安装的包数量众多。
React、Vue、Angular 和 Svelte 等主流框架都有对应版本,搜索“框架名 + 模块名”(如 “React Helmet”)即可找到。Meta tags、Helmet 与 Head 都属于功能相近的热门模块,用于设置 SEO 所需的各类常用标签。
某些方面,JavaScript 优于传统 HTML,例如构建便利性与性能;某些方面则更差,例如它无法像 HTML 和 CSS 那样渐进式解析,且在页面加载与性能上可能偏重。很多时候,是以性能换取功能。
JavaScript 并非完美,也并非所有问题的合适解法。开发者确实会过度使用它,去处理本可有更好方案的问题。但有时,只能就地取材。
JavaScript SEO 的常见问题与最佳实践
以下列举处理 JavaScript 网站时常见的 SEO 问题。
确保标题标签与元描述独一无二
各页面仍应拥有独一无二的标题标签与元描述。许多 JavaScript 框架采用模板化方式,容易造成所有页面或某一组页面共用同一标题或元描述。
在 Ahrefs Site Audit(站点审计)中查看重复项(Duplicates)报告,点击任意分组即可查看更详细的数据。

可以使用 Helmet 之类的 SEO 模块为每个页面设置自定义标签。
JavaScript 也能用来覆盖你设置的默认值。Google 会处理这一过程,并采用覆盖后的标题或描述。但对用户而言,标题可能带来问题:浏览器中先显示一个标题,被覆盖时会出现闪烁。
若出现标题闪烁,可用 Ahrefs 的 SEO Toolbar(SEO 工具栏)同时查看原始 HTML 与渲染后的版本。

无论怎样,Google 都可能不使用你的标题或元描述。如前所述,标题值得为用户体验整理干净;但修好元描述带来的差别其实有限。
在研究 Google 的重写行为时我们发现,Google 有 33.4% 的情况下会改写标题,62.78% 的情况下会改写元描述。Site Audit 中甚至会指出 Google 改写了你哪些标题标签。

规范标签(Canonical tag)问题
多年来,Google 一直表示不认可通过 JavaScript 插入的规范标签。后来它在文档中新增了一个例外,适用于原本尚无该标签的情况。这一变更正是由我促成——我通过测试证明,这种方法在 Google 告知所有人它不管用时其实是有效的。
若页面上已有规范标签,又用 JavaScript 添加了另一个或覆盖了原有的那个,就等于给了 Google 两个规范标签。此时 Google 必须自行判断采用哪一个,或直接忽略这些规范标签,转而参考其他规范化信号。
“每个页面都应有一个指向自身的规范标签”这条标准建议,让许多 SEO 陷入麻烦。开发人员照此执行,会让带尾斜杠和不带尾斜杠的页面都自引用规范。
[['Text', 'example.com/page']] 的规范标签是 [['Text', 'example.com/page']],而 [['Text', 'example.com/page/']] 的规范标签是 [['Text', 'example.com/page/']]。这就错了。更合理的做法,是把其中一个版本重定向到另一个。
带参数的版本同理——你可能想把这些版本合并,但它们各自指向自身。
Google 会采用最严格的指令
对 meta robots 标签,Google 总会采用它看到的最严格选项,无论它出现在哪里。
若原始 HTML 中有 index 标签,而渲染后的 HTML 中有 noindex 标签,Google 会将其视为 noindex。若原始 HTML 中有 noindex 标签,你用 JavaScript 把它覆盖成 index,Google 仍会把该页面当作 noindex 处理。事实上,带 noindex 的页面根本不会进入渲染器。
nofollow 标签同理。Google 会采用最严格的选项。
为图片设置 alt 属性
缺失alt 属性属于无障碍(可访问性)问题,可能演变成法律问题。多数大公司都曾因网站的无障碍(ADA)合规问题被起诉,有些公司一年内被起诉多次。主要内容中的图片建议补上,占位图或装饰性图片可以留空 alt 属性。
对网页搜索而言,alt 属性中的文本会被算作页面文本,但这也仅限于此。它的 SEO 重要性常被高估。不过它确实有助于图片搜索与图片排名。
许多 JavaScript 开发者会把 alt 属性留空,需仔细检查图片是否都已设置。可在 Site Audit 中查看图片(Images)报告定位这些问题。

允许抓取 JavaScript 文件
若某些资源是构建页面或添加内容所必需的,就不要阻止对它们的访问。Google 需要访问并下载这些资源,才能正确渲染页面。在 robots.txt 中,允许抓取所需资源最简单的方式是添加:
[['Text', 'User-Agent: Googlebot']][['Text', 'Allow: .js']][['Text', 'Allow: .css']]
同时要检查可能发起请求的子域名或额外域名(例如用于 API 调用的域名)的 robots.txt 文件。
若已用 robots.txt 屏蔽了某些资源,可在 Chrome 开发者工具的“Network(网络)”标签中使用屏蔽选项,检验它是否影响页面内容。选中该文件并屏蔽,然后重新加载页面,观察是否有变化。

检查 Google 是否能看到你的内容
许多带 JavaScript 功能的页面,默认可能不会向 Google 展示全部内容。与开发人员沟通时,他们可能称之为“未加载 DOM”,即内容并非默认加载,而要等到点击等操作之后才加载。
一种快速的检查方式,是在 Google 中用引号搜索内容中的一小段。搜索“你内容中的某个短语”,看该页面是否出现在结果中。若出现,则内容很可能已被看到。
也可以右键点击并选择“Inspect(检查)”,在“Elements(元素)”标签中搜索该文本。

最好的检查方式,是在 Google 的测试工具(如 Google Search Console 中的网址检查工具)的内容中搜索。这一点稍后会详述。
藏在折叠面板(accordion)或下拉菜单里的内容务必检查。很多时候,这些元素会在被点击时发起请求,把内容加载进页面。而 Google 不会点击,因此看不到这些内容。
若用检查(inspect)方法搜索内容,务必先复制内容,然后在搜索前重新加载页面或在无痕窗口中打开。
若点击了该元素、内容在操作时才加载进来,就能找到内容;但页面全新加载时,可能看不到同样的结果。
重复内容问题
使用 JavaScript 时,同一份内容可能对应多个 URL,进而引发重复内容问题。这可能是由大小写、尾斜杠、ID、带 ID 的参数等造成的。因此下面这些 URL 可能同时存在:
[['Text', 'domain.com/Abc']][['Text', 'domain.com/abc']][['Text', 'domain.com/123']][['Text', 'domain.com/?id=123']]
若只想索引其中一个版本,应设置一个指向自身的规范标签,并让其他版本通过规范标签指向主版本;更好的做法,是把其他版本重定向到主版本。
在 Site Audit 中查看重复项(Duplicates)报告。我们会细分哪些重复内容簇已设置规范标签、哪些存在问题。

JavaScript 框架的一个常见问题是,页面可能同时以带尾斜杠和不带尾斜杠的形式存在。理想做法是选定偏好版本,确保该版本有指向自身的规范标签,然后把另一个版本重定向到偏好版本。
采用应用外壳(app shell)模式时,初始 HTML 响应中可能只包含很少的内容与代码。事实上,网站上的每个页面可能都显示相同的代码,而且这段代码可能与其他某些网站完全相同。
若在 Site Audit 中看到大量字数很低的 URL,可能意味着存在这一问题。

这有时会导致页面被当作重复内容处理,而不会立即进入渲染。更糟的是,搜索结果中可能显示错误的页面,甚至错误的网站。这一问题通常会随时间自行解决,但也可能造成麻烦,尤其是对较新的网站。
不要在 URL 中使用片段(#)
# 在浏览器中已有明确定义的功能。点击它会链接到页面的另一部分,正如我们博客上的“目录”功能。服务器通常不处理 # 之后的内容。因此对像 [['Text', 'abc.com/#something']] 这样的 URL,井号(#)之后的内容通常会被忽略。
JavaScript 开发者却想用 # 实现其他各种目的,由此造成混乱。最常见的误用是把它用于路由和 URL 参数。这样做确实能工作,但本不该如此使用。
JavaScript 框架通常带有路由器,用于把他们所说的路由(路径)映射到干净的 URL。不少 JavaScript 开发者用井号(#)来做路由,对 Vue 和早期某些版本的 Angular 来说尤其成问题。
要为 Vue 修复这个问题,可与开发人员一起修改以下内容:
[['Text', 'Vue router: ']][['Text', '改用 “History” 模式,而不是传统的 “Hash” 模式。']]
[['Text', 'const router = new VueRouter ({']][['Text', 'mode: ‘history’,']][['Text', 'router: [] //路由链接数组']][['Text', ')}']]
有一种日益流行的趋势,人们用 # 代替 ? 作为片段标识符,尤其用于像跟踪这类被动型 URL 参数。这种做法我并不推荐,它带来的混乱和问题不少。在特定情况下,如果能借此去掉大量不必要的参数,或许可以接受。
创建站点地图
那些支持干净 URL 的路由器选项通常还带有额外模块,也能生成站点地图。可搜索“你的系统 + router sitemap”找到它们,例如 “Vue router sitemap”。
许多渲染方案可能也提供站点地图选项。同样,找到你使用的系统并在 Google 上搜索“系统 + sitemap”,例如 “Gatsby sitemap”,一般都能找到现成方案。
状态码与软 404
由于 JavaScript 框架并非服务器端,它们无法真正抛出像 404 这样的服务器错误。对错误页面,有几种不同选择,例如:
- 使用 JavaScript 重定向到一个确实返回 404 状态码的页面。
- 给出错的页面添加 noindex 标签,并附上某种错误提示,比如“404 Page Not Found(页面未找到)”。由于实际返回的状态码是 200 OK,这会被视为软 404。
JavaScript 重定向可以接受,但并非首选
SEO 习惯于 301/302 重定向,它们属于服务器端。JavaScript 通常在客户端运行。对 Google 来说,服务器端重定向乃至 meta refresh 重定向都比 JavaScript 重定向更容易处理,因为前者无需渲染页面即可看到。
JavaScript 重定向在渲染过程中仍会被看到并处理,多数情况下没问题,只是不如其他重定向类型理想。它会被视为永久重定向,并仍然传递像 PageRank 这样的全部信号。
通常可在代码中查找 “window.location.href” 来找到这些重定向。重定向也可能位于配置文件中。在 Next.js 配置里,有一个 redirect 函数可用于设置重定向;在其他系统中,可能会在路由器里找到。
国际化问题
不同框架通常都有几个模块选项,支持 hreflang 之类的国际化所需功能。它们通常已被移植到各种系统中,包括 i18n、intl;很多时候,用于处理头部标签的同一模块(如 Helmet)也能用来添加所需标签。
我们会在 Site Audit 的本地化(Localization)报告中标记 hreflang 问题。我们还做过一项研究,发现使用 hreflang 的域名中有 67% 存在问题。

若你的网站以不同方式屏蔽或区别对待来自特定国家/地区的访客,或使用特定 IP 的访客,也需谨慎。这可能导致内容无法被 Googlebot 看到。若你有重定向用户的逻辑,或许应把机器人排除在该逻辑之外。
在 Site Audit 中设置项目时,我们会告知是否发生了这种情况。

使用结构化数据
JavaScript 可用于生成或注入页面上的结构化数据。用 JSON-LD 来做很常见,也不太可能引发问题,但仍要运行测试,确认输出符合预期。
我们会在 Site Audit 的问题(Issues)报告中标记发现的结构化数据问题。查找 “Structured data has schema.org validation” 错误,我们会确切指出每个页面出了什么问题。

使用标准格式的链接
指向其他页面的链接应采用网络标准格式。内链和外链都必须是带 [['Text', 'href']] 属性的 [['Text', '<a>']] 标签。在 JavaScript 环境下,有很多方法能让链接对用户可用,却对搜索不友好。
好的做法:
[['Text', '<a href=”/page”>simple is good</a>']]
[['Text', '<a href=”/page” onclick=”goTo(‘page’)”>still okay</a>']]
坏的做法:
[['Text', '<a onclick=”goTo(‘page’)”>nope, no href</a>']]
[['Text', '<a href=”javascript:goTo(‘page’)”>nope, missing link</a>']]
[['Text', '<a href=”javascript:void(0)”>nope, missing link</a>']]
[['Text', '<span onclick=”goTo(‘page’)”>not the right HTML element</span>']]
[['Text', '<option value="page">nope, wrong HTML element</option>']]
[['Text', '<a href=”#”>no link</a>']]
Button、ng-click,和其他做法还会以更多方式把这件事做错。
据我的经验,Google 仍会处理并抓取许多“坏”链接,但它在传递 PageRank 之类的信号时如何对待它们,我并不确定。网络世界很杂乱,Google 的解析器往往较为宽容。
同样需要指出,用 JavaScript 添加的内链要到渲染之后才会被识别。渲染通常相对较快,多数情况下不必担心。
用文件版本控制解决“不可能的状态”被索引的问题
Google 会在自己一端大量缓存所有资源。它的系统可能导致一些“不可能的状态”被索引,这是其系统的怪癖。在这些情况下,旧的文件版本会被用于渲染,页面被索引的版本可能包含旧文件的某些部分。
可以使用文件版本控制或指纹(file.12345.js),在做出重大改动时生成新的文件名,这样 Google 就必须下载更新后的资源版本用于渲染。
你可能看不到展示给 Googlebot 的内容
诊断某些问题时,可能需要更改 user-agent。内容可能因不同的 user-agent 甚至 IP 而呈现不同。应该用 Google 的测试工具检查它实际看到的是什么,稍后会讲到这些工具。
可在 Chrome DevTools 中设置自定义 user-agent,以排查那些会基于特定 user-agent 预渲染的站点;也可以用我们的工具栏轻松完成。

为不支持的功能使用 polyfill
开发者可能用到 Googlebot 不支持的某些功能。你的开发者可以使用功能检测。若有缺失的功能,他们可以跳过该功能,或用 polyfill 作为降级方案,看能否让它正常工作。
这主要是给 SEO 的一个提醒:如果你认为 Google 应该看到某些内容却看不到,问题可能出在实现方式上。
使用懒加载
自本文最初撰写以来,懒加载大多已从由 JavaScript 驱动转变为由浏览器处理。
你可能仍会遇到一些由 JavaScript 驱动的懒加载方案。多数情况下,若懒加载对图片,问题不大。最需要检查的是内容是否被懒加载,参见上文“检查 Google 是否能看到你的内容”一节。这类方案曾导致内容无法被正确识别。
无限滚动问题
若采用了无限滚动,仍建议提供一个分页版本,以便 Google 正常抓取。
这类方案中还存在另一个问题:偶尔会有两个页面被当作一个页面索引。我见过数次,用户说页面无法被索引,实际却是其内容作为另一个页面的一部分被索引了,而那通常是他们的上一篇博文。
我的推测是,当 Google 调整视口使其更长时(稍后详述),触发了无限滚动,在渲染时又加载了另一篇文章。对这种情况,建议屏蔽处理无限滚动的那个 JavaScript 文件,使该功能无法被触发。
性能问题
许多 JavaScript 框架会替你处理好大量现代性能优化工作。
所有传统的性能最佳实践依然适用,此外还能用到一些新选项。代码分割(code splitting)会把文件拆分成更小的文件;摇树优化(tree shaking)会挑出需要的部分,进而不必像传统单体式方案那样为每个页面都加载全部内容。
做得好的 JavaScript 方案令人赏心悦目;做得不好的则可能臃肿不堪,导致加载时间过长。
网站性能的更多内容,参见核心网页指标指南。
JavaScript 网站会消耗更多抓取预算
JavaScript 的 XHR 请求会大量消耗抓取预算。与多数会被缓存的资源不同,这些请求会在渲染过程中实时获取。
另一个细节是,渲染服务会尽量不去获取那些对页面内容没有贡献的资源。若它判断失误,你可能就会缺失一些内容。
Web Worker 不受支持,还是另有可能?
Google 历来表示会拒绝 service worker,而且 service worker 无法编辑 DOM;不过 Google 自家的 Martin Splitt 表示,有时或许能绕过这一限制。
使用 HTTP 连接
Googlebot 支持 HTTP 请求,但不支持其他连接类型,比如 WebSockets 或 WebRTC。若使用了这些,请提供一个使用 HTTP 连接的降级方案。
JavaScript SEO 测试工具与问题排查
JavaScript 网站的一个难点,在于它们可能对 DOM 做局部更新。用户浏览到另一个页面时,DOM 中的某些内容(如标题标签或规范标签)可能不会更新,但这对搜索引擎通常不成问题。
Google 会把每个页面都当作全新加载、无状态地加载,既不保存之前的信息,也不在页面之间导航。
我见过一些 SEO 因为从一个页面导航到另一个页面后看到的情况(比如规范标签没有更新)而误以为出了问题,但 Google 可能根本看不到这个状态。
开发者可以通过使用所谓的 History API 更新状态来解决问题。但再强调一次,这可能并不是问题。很多时候,只是 SEO 觉得看起来奇怪,反而给开发者添了麻烦。刷新一下页面,看看会看到什么;更好的办法是,用 Google 的某个测试工具跑一遍,看看它看到的是什么。
说到 Google 的测试工具,我们来谈谈它们。
Google 测试工具
Google 有几个对 JavaScript 很有用的测试工具。
Google Search Console 中的网址检查工具
这应该是你的“事实来源(source of truth)”。检查某个网址时,你会得到大量有关 Google 所见内容的信息,和来自其系统的实际渲染后 HTML。

也可以选择运行实时测试。

主渲染器与实时测试之间存在一些差异。渲染器使用缓存的资源,且很有耐心。实时测试和其他测试工具使用实时资源,会提前终止渲染,因为用户在等待结果。稍后的渲染章节会详细说明这一点。
这些工具中的截图还会显示已经完成像素绘制的页面,而 Google 在渲染页面时并不会这样做。
这些工具有助于查看内容是否已加载到 DOM 中。工具中显示的 HTML 就是渲染后的 DOM,可搜索一小段文本,看它是否默认加载。

这些工具还会显示可能被屏蔽的资源和控制台错误信息,对调试很有帮助。
若你无法访问某个网站的 Google Search Console 媒体资源,仍可对它运行实时测试。在你拥有 Google Search Console 访问权限的媒体资源上添加一个重定向,然后检查那个网址,检查工具会跟随该重定向,并显示另一个域名上该页面的实时测试结果。
在下面的截图中,我从自己的网站添加了一个指向 Google 首页的重定向。它的实时测试会跟随该重定向,向我显示 Google 的首页。而我实际上并没有 Google 的 Google Search Console 账户的访问权限,虽然我希望有。

富媒体搜索结果测试(Rich Results Test)工具
富媒体搜索结果测试工具允许你以 Googlebot 在移动端或桌面端所看到的方式来检查渲染后的页面。
移动设备适合性测试(Mobile-Friendly Test)工具
你目前仍可以使用移动设备适合性测试工具,但 Google 已宣布它将于 2023 年 12 月停用。
它和 Google 的其他测试工具有同样的怪癖。
Ahrefs
Ahrefs 是唯一一款在抓取网络时会渲染网页的主流 SEO 工具,因此我们拥有其他工具都没有的、来自 JavaScript 网站的数据。我们每天渲染约 2 亿个页面,但这只是抓取量中很小的一部分。
这让我们能够检查 JavaScript 重定向,也能展示我们发现的用 JavaScript 插入的链接——在链接报告中会用 JS 标签标记:

在 Site Explorer 中页面下拉菜单里,我们还有一个检查(inspect)选项,让你查看页面的历史记录并与其他抓取结果对比。对启用 JavaScript 渲染过的页面,那里有一个 JS 标记。

你可以在 Site Audit 抓取中启用 JavaScript,进而在审计中解锁更多数据。

在 Site Audit 中免费检查 JavaScript SEO 问题
启用 JavaScript 渲染后,我们会为每个页面提供原始 HTML 和渲染后的 HTML。使用 Page Explorer 中页面旁边的“放大镜”选项,然后在菜单中进入 “View source(查看源代码)”。你还可以与之前的抓取对比,并在全站所有页面的原始或渲染后 HTML 中搜索。

若先在不启用 JavaScript 的情况下运行一次抓取,再启用它运行一次,就可以用我们的抓取对比功能查看各版本之间的差异。

Ahrefs 的 SEO Toolbar 也支持 JavaScript,让你把 HTML 与标签的渲染版本对比。

查看源代码与检查(Inspect)
在浏览器窗口中右键点击时,会看到几个用于查看页面源代码和检查页面的选项。查看源代码所显示的,与一次 GET 请求所得到的一致,这就是页面的原始 HTML。

检查(Inspect)显示的是经过更改后的已处理 DOM,更接近 Googlebot 所看到的内容。它是 JavaScript 运行并对页面做出更改之后的页面。
处理 JavaScript 时,多数情况应使用检查(inspect)而不是查看源代码(view source)。
有时仍然需要查看源代码
由于 Google 对某些问题会同时查看原始 HTML 和渲染后的 HTML,所以有时仍需要查看源代码。例如,若 Google 的工具告知页面被标记为 noindex,而你在渲染后的 HTML 中看不到 noindex 标签,那它很可能原本存在于原始 HTML 中,后来被覆盖了。
对 noindex、nofollow 和规范标签这类内容,可能需要查看原始 HTML,因为问题可能会延续过来。记住,对 meta robots 标签,Google 会采用它看到的最严格的说法;而当你展示多个规范标签时,它会忽略这些规范标签。
不要在关闭 JavaScript 的情况下浏览网页
这条建议被推荐了太多次。Google 会渲染 JavaScript,所以你在关闭 JavaScript 时看到的内容,与 Google 看到的完全不一样。这种做法没有意义。
不要使用 Google Cache(网页快照)
Google 的缓存并不是检查 Googlebot 所见内容的可靠方式。你在缓存中通常看到的是原始 HTML 快照,随后浏览器会执行 HTML 中引用的 JavaScript。这并不是 Google 渲染页面时看到的内容。
更复杂的是,网站可能把它们的跨源资源共享(CORS)策略设置成无法从不同域名加载所需资源。
缓存托管在 webcache.googleusercontent.com 上。当该域名尝试从实际域名请求资源时,CORS 策略会说:“不行,你不能访问我的文件。”于是文件加载不出来,页面在缓存中看起来就是坏的。
缓存系统的本意是在网站宕机时查看其内容,作为调试工具并不特别有用。
在搜索引擎的早期,一次下载下来的 HTML 响应就足以看到大多数页面的内容。由于 JavaScript 的兴起,搜索引擎现在需要像浏览器那样渲染许多页面,才能看到用户所看到的内容。
Google 负责处理渲染过程的系统叫做 Web Rendering Service(WRS,网络渲染服务)。Google 提供了一张简化的示意图说明这个过程如何运作。

假设我们从 URL 开始这个流程。
1. 爬虫(Crawler)
爬虫向服务器发送 GET 请求。服务器返回响应头和文件内容,随后这些内容会被保存下来。响应头和内容通常来自同一次请求。
由于 Google 现在采用移动优先索引,请求很可能来自移动端 user-agent,但它也仍然使用桌面端 user-agent 抓取。
请求大多来自山景城(美国加州),但它也会对美国以外的某些面向地区自适应的页面抓取。如前面所述,如果网站以不同方式屏蔽或区别对待特定国家的访客,这可能引发问题。
同样需要注意,虽然 Google 在上图中把抓取过程的输出标为“HTML”,但实际上,它抓取并存储的是构建页面所需的资源,如 HTML、JavaScript 文件和 CSS 文件。此外,HTML 文件还有 15 MB 的最大体积限制。
2. 处理(Processing)
上图中的“处理(Processing)”一词掩盖了许多系统。以下讲述其中几个与 JavaScript 相关的系统。
资源与链接
Google 不会像用户那样从一个页面导航到另一个页面。“处理”的一部分工作是检查页面中指向其他页面的链接和构建页面所需的文件。这些链接会被提取出来并加入抓取队列,Google 正是用它来对抓取优先级排序和调度。

Google 会从 [['Text', '<link>']] 标签之类的地方提取构建页面所需的资源链接(CSS、JS 等)。
如前所述,用 JavaScript 添加的内链要到渲染之后才会被识别。渲染通常相对较快,多数情况下不必担心。像新闻网站这类每一秒都很关键的场景可能是例外。
缓存
Google 下载的每个文件,包括 HTML 页面、JavaScript 文件、CSS 文件等,都会被积极缓存。Google 会忽略你的缓存时间设置,在它想要的时候获取新副本。有关这一点及其重要性,会在“渲染器”一节再作说明。

重复内容消除
重复内容可能会在下载的 HTML 被送往渲染之前就被消除或降低优先级。这一点已在上文“重复内容问题”一节讲过。
最严格的指令
如前所述,Google 会在 HTML 与页面渲染版本之间选择最严格的说法。如果 JavaScript 更改了某个说法,而它与 HTML 中的说法相冲突,Google 就会遵从其中限制最严格的那个。noindex 会覆盖 index,而 HTML 中的 noindex 会完全跳过渲染。
3. 渲染队列(Render queue)
许多 SEO 对 JavaScript 和两阶段索引(先 HTML 后渲染页面)的最大担忧,是页面可能要过几天甚至几周才会被渲染。Google 调查这一点时发现,页面进入渲染器的中位时间是五秒,第 90 百分位是几分钟。因此多数情况下,得到 HTML 与渲染页面之间的时间间隔不必担忧。
不过,Google 并不会渲染所有页面。如我之前所说,带有 robots meta 标签或响应头中包含 noindex 标签的页面不会被送往渲染器。它不会浪费资源去渲染一个反正也无法索引的页面。
在此过程中它还设有质量检查。如果它查看了 HTML,或能从其他信号或规律中合理判断出某个页面质量不够好、不值得索引,就不会费心把该页面送往渲染器。
新闻网站还有一个怪癖。Google 希望能快速索引新闻网站上的页面,所以会先基于 HTML 内容索引这些页面,之后再回来渲染它们。
4. 渲染器(Renderer)
渲染器是 Google 渲染页面、查看用户所见内容的地方。它正是在这里处理 JavaScript 和 JavaScript 对 DOM 所做的任何更改。

为此,Google 使用了一个如今已是“常青(evergreen)”的无头 Chrome 浏览器,这意味着它应使用最新的 Chrome 版本并支持最新功能。多年前,Google 是用 Chrome 41 来渲染的,那时许多功能都不受支持。
Google 有更多有关 WRS 的信息,包括拒绝权限、无状态、扁平化 light DOM 和 shadow DOM 等内容,值得一读。
网页规模级别的渲染或许是世界第八大奇迹。这是一项艰巨的工程,需要耗费海量资源。正因规模如此之大,Google 在渲染过程中采用了许多捷径来提速。
缓存资源
Google 高度依赖缓存资源。页面会被缓存,文件会被缓存,几乎所有东西在送往渲染器之前都会被缓存。它不会为每次页面加载都去下载每个资源,因为那样对它自己和网站主来说成本都很高。相反,它使用这些缓存资源来提高效率。
例外是 XHR 请求,渲染器会实时发起这些请求。
并不存在五秒超时
一个常见的 SEO 迷思是,Google 只等五秒来加载你的页面。虽然让网站更快始终是个好主意,但这个迷思与上文提到的 Google 缓存文件的方式根本不符。它加载页面时,其系统中的所有东西都已缓存,并不会去请求新的资源。

如果它只等五秒,那会漏掉大量内容。
这个迷思很可能来自像网址检查工具这样的测试工具——其中资源是实时获取而非缓存的,而且它们需要在合理的时间内向用户返回结果。它也可能来自页面未被优先抓取的情况,这让人们以为等待渲染和索引花了很长时间。
渲染器没有固定的超时时间。它以一个加速的计时器运行,看看之后是否有内容被添加进来。它还会观察浏览器中的事件循环,看所有操作何时完成。它有足够的耐心,不必担心任何具体的时间限制。
它虽然有耐心,但也设有保护机制,以防某些东西卡住或者有人试图在其页面上挖比特币。是的,真有这种事。我们也不得不对比特币挖矿添加保护措施,甚至发表了一项相关研究。
Googlebot 看到的内容
Googlebot 不会对网页执行操作。它不会点击或滚动,但这并不意味着它没有变通办法。只要内容无需操作就已加载进 DOM,Google 就能看到它。如果内容要到点击之后才加载进 DOM,那么这些内容就不会被发现。
Google 也不需要滚动就能看到你的内容,因为它有一个巧妙的变通办法。在移动端,它加载页面时屏幕尺寸为 411x731 像素,并把长度调整为 12,140 像素。
本质上,它变成了一部长得离谱的手机,屏幕尺寸为 411x12140 像素。在桌面端,它同样如此,从 1024x768 像素变为 1024x9307 像素。我没有看到过有关这些数字的近期测试,而且它可能会根据页面长度而变化。

另一个有趣的捷径是,Google 在渲染过程中不会绘制像素。完成页面加载需要时间和额外资源,而它并不需要看到像素绘制完成后的最终状态。再说,显卡在游戏、加密货币挖矿和 AI 的多重夹击下价格不菲。
Google 只需要知道结构和布局,而它无需真正绘制像素就能获得这些。正如 Martin 所说:
在 Google 搜索中,我们并不关心像素,因为我们并不是真的要把它展示给某个人看。我们要处理的是信息和语义信息,所以需要一个中间状态。我们不必真的去绘制像素。
一张示意图或许能更好地说明被省略掉的部分。在 Chrome 开发者工具中,如果你在“Performance(性能)”标签上运行一次测试,会得到一张加载图表。这里实心的绿色部分代表绘制(painting)阶段。对 Googlebot 来说,这一步永远不会发生,因此节省了资源。

灰色 = 下载(Downloads)蓝色 = HTML黄色 = JavaScript紫色 = 布局(Layout)绿色 = 绘制(Painting)
5. 抓取队列(Crawl queue)
Google 有一份资源(文档)稍微谈到了抓取预算。你应该知道,每个网站都有自己的抓取预算,每个请求都必须排定优先级。Google 还必须在抓取你的页面与抓取互联网上所有其他页面之间取得平衡。
总体而言,较新的网站或者拥有大量动态页面的网站,抓取速度可能会更慢。有些页面的更新频率会低于其他页面,有些资源的请求频率也可能更低。
在渲染 JavaScript 方面有很多选择。Google 有一张很不错的图表,我就直接放上来。任何形式的 SSR、静态渲染和预渲染方案对搜索引擎来说都没问题。Gatsby、Next、Nuxt 等都是很好的选择。

问题最大的是完全客户端渲染,即所有渲染都发生在浏览器中。虽然 Google 可能对客户端渲染没问题,但最好选择不同的渲染方案,以支持其他搜索引擎。
Bing 也支持 JavaScript 渲染,但规模未知。据我所见,Yandex 和百度有有限的支持,而许多其他搜索引擎对 JavaScript 几乎或完全不支持。我们自家的搜索引擎 Yep 支持它,我们每天渲染约 2 亿个页面。但我们不会渲染抓取到的每一个页面。
还有一种选择是动态渲染(dynamic rendering),即对特定 user-agent 渲染。这是一种变通办法,说实话,我从没推荐过它,也很高兴 Google 现在也建议不要使用它。
在特定情况下,你可能想用它来为某些机器人渲染,比如搜索引擎甚至社交媒体机器人。社交媒体机器人不运行 JavaScript,所以除非你在把内容提供给它们之前先渲染好,否则 OG 标签之类的内容不会被看到。
实际上,它会让方案更复杂,也更难让 SEO 排查问题。它绝对是伪装(cloaking),尽管 Google 说它不是、还说可以接受。
写在最后
JavaScript 不该是 SEO 害怕的东西。希望这篇文章能帮助你更好地理解如何应对它。
不要害怕联系你的开发者、与他们合作并向他们提问。在帮助你为搜索引擎优化 JavaScript 网站这件事上,他们会是你最好的盟友。
有问题吗?在 Twitter 上告诉我。