服务端渲染与 SEO:为什么源码里必须有正文

从爬虫的两阶段索引机制讲清 CSR 为什么拖慢收录甚至进不了索引,SSR 与预渲染为什么对爬虫友好;附四种自查方法、SSR 也会翻车的五个坑与按页面职责分的渲染选型表。

  • SEO
  • 概念
  • 入门
  • 更新于 2026-10-31

做技术内容站最容易踩的坑,不是关键词没选好,而是发布三个月了,搜索结果里一条都没有。原因往往不在内容本身,而在渲染方式:你写给搜索引擎看的正文,压根没出现在它第一次抓取时拿到的 HTML 里。

这篇把这件事从头讲透:搜索引擎到底分几步读你的页面、客户端渲染具体会造成哪些可观察的后果、怎么用五分钟自证自己的页面是不是安全的,最后给一张按页面职责分的渲染选型表。你读完应该能自己判断一个站点该用 SSR、SSG 还是 ISR,以及怎么在不发版的情况下验证爬虫到底看到了什么。

一、搜索引擎读一个页面,其实分两步

这是所有误解的源头。搜索引擎处理一个 URL 时,并不是「拿到 HTML → 立刻索引」这么简单,而是两个阶段:

第一阶段:抓取(Crawl) 爬虫下载原始 HTML。这一步拿到什么,它就登记什么:发现了哪些链接、页面里有哪些文字、<head> 里有没有 title 和 description。如果 HTML 是个空壳,这一阶段登记到的就是「一个空壳」。

第二阶段:渲染(Render) 对于需要执行 JavaScript 的页面,搜索引擎会把它放进一个渲染队列,由渲染服务执行 JS、等待接口请求完成,再把渲染后的 DOM 拿去做真正的索引。这一步叫「第二波索引」,因为它是发生在第一波抓取之后的第二次处理。

关键在于:第二波不是免费的。渲染队列有优先级,取决于站点权重、页面重要性、外链、更新频率和抓取预算分配。实践中,从第一波抓取到第二波渲染完成,间隔可以是几小时,也可以是几天;权重低的新站或站点深处的页面,甚至可能长期排不上队。这正是「发布了但一直没被收录」最常见的技术成因——不是内容被判低质,而是根本没进第二轮。

不执行 JavaScript 的爬虫,问题更大

Googlebot 是目前最「能扛事」的爬虫,它会执行 JS。但生态里还有一批爬虫不会:

爬虫类型 典型代表 是否执行 JS 拿不到就意味着
搜索引擎主爬虫 Googlebot 会(需排队) 收录延迟,可能永不收录
社交卡片抓取器 Facebook、LinkedIn、Slack 不会 分享出去没有标题和封面图
AI 检索爬虫 各类答案引擎的抓取器 多数不会 内容无法被引用
部分 Bing 配置 某些部署形态 不保证 索引质量不稳定

社交分享这一条最容易被忽略:链接发到群里,标题和封面是抓取器读 <head> 里的 og: 标签生成的。如果这些标签是前端 JS 注入的,抓取器看到的就是一片空白——卡片没有标题、没有图,点进去才知道是什么内容。

二、客户端渲染的四个可观察症状

把上面的机制对着自己的站点看,CSR(纯客户端渲染)的问题会表现为四种具体现象,而不是一句「SEO 不好」:

1. 新页面收录延迟甚至不收录 内容发布了,站点地图也提交了,但搜索结果里迟迟不出现。查看「已呈现的 HTML」会看到内容在——说明第一波没拿到,是第二波渲染没赶上或没轮到。

2. 分享卡片是空的 链接发出去只有域名,没有标题、描述和封面图。这是社交抓取器读不到 og: 标签的直接后果,与页面在浏览器里显示得漂不漂亮无关。

3. 元数据被前端注入,爬虫读不到 title、meta description、canonical、结构化数据如果写在客户端组件里,爬虫在第一阶段拿到的是没有这些标签的 HTML,等它渲染完再补上——而很多爬虫根本不补。

4. 首屏指标变差,拖累排名与转化 HTML 里没有内容时,浏览器要等 JS 下载、解析、执行、请求接口,才把内容画出来。首次内容绘制(FCP)和最大内容绘制(LCP)都被推后,布局还容易在注水时跳动(CLS)。这不只是体验问题——Core Web Vitals 本身是排名信号。

一个具体的对照

同样是「文章详情页」,两种实现给爬虫的东西完全不同。

客户端渲染返回的 HTML:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title></title>
  </head>
  <body>
    <!-- 正文要等 JS 执行后才会出现 -->
    <div id="root"></div>
    <script src="/static/js/main.8f3a21.js" defer></script>
  </body>
</html>

服务端渲染返回的 HTML:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Koa 中间件实战:从洋葱模型到完整 API 骨架</title>
    <meta name="description" content="Koa 中间件是接收 ctx 与 next 的 async 函数…" />
    <link rel="canonical" href="https://example.com/blog/koa-onion" />
  </head>
  <body>
    <article>
      <h1>Koa 中间件实战:从洋葱模型到完整 API 骨架</h1>
      <p>Koa 的核心是中间件……</p>
    </article>
  </body>
</html>

第二份 HTML 里,标题、摘要、正文、链接一应俱全。任何爬虫——包括不执行 JS 的——拿到它都能直接建立索引。

三、SSR、SSG、ISR 对 SEO 意味着什么

关键结论先说:SSR 和预渲染(SSG / ISR)在 SEO 上是同一阵营,它们都把完整 HTML 交给爬虫;客户端渲染是另一阵营。差别不在「能不能被收录」,而在「内容有多新」和「服务器扛不扛得住」。

维度 客户端渲染 CSR 服务端渲染 SSR 静态生成 SSG 增量静态再生 ISR
HTML 什么时候生成 浏览器里 每次请求 构建时 构建时 + 按周期重建
爬虫第一次拿到 空壳 完整正文 完整正文 完整正文
收录表现 延迟,甚至不收录 正常 最好 正常
内容新鲜度 实时 实时 直到下次构建 有过期时间
服务器压力 低 每次请求都算 几乎为零 只有重建时算
首屏速度 慢(等 JS) 中 最快 快(命中缓存时)
典型场景 登录后的面板 搜索结果页、个性化页 文档、落地页 文章、商品详情

ISR 的工作方式值得单独说,因为它是内容站的甜点:

  1. 构建阶段,页面被预渲染成静态 HTML 并缓存;
  2. 用户请求到达,如果缓存没过期,直接返回这份静态 HTML(快,且能被 CDN 缓存);
  3. 缓存过期时,先把旧内容返回给用户,同时在后台异步触发这个页面的重新生成,不阻塞当前请求;
  4. 下一次请求拿到的就是更新后的 HTML。

也就是常说的 stale-while-revalidate:用户永远不等待重建,只有「被访问到的页面」会重建,而不是全站重新构建一遍。

四、渲染方式之外的四个变量

渲染方式决定了「爬虫能不能读到正文」,但它不是唯一变量。下面四项同样会直接影响收录与排名,实践里经常和渲染问题混在一起被误判。

站点地图:决定爬虫能发现哪些页面

爬虫靠链接发现页面,但靠链接发现有天然盲区:孤岛页面(没有任何页面链向它)可能永远进不了队列。sitemap.xml 就是显式告诉爬虫「这些 URL 存在」的清单。

// app/sitemap.ts —— Next.js 自动生成 /sitemap.xml
import type { MetadataRoute } from 'next';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const articles = await listPublishedArticles();

  return [
    { url: 'https://example.com', lastModified: new Date(), changeFrequency: 'daily', priority: 1 },
    ...articles.map((a) => ({
      url: `https://example.com/articles/${a.id}`,
      lastModified: a.updatedAt,
      changeFrequency: 'weekly' as const,
      priority: 0.8,
    })),
  ];
}

单个站点地图上限 5 万条 URL;更大的站点要拆分成多个文件。站点地图里的 URL 必须与站点实际返回的 URL 一致——指向 404 的地址会浪费抓取预算。

内部链接:决定内容深度与权重如何流动

爬虫顺着链接爬,链接结构因此决定了页面被发现的速度与被赋予的权重:

  • 孤岛页面没有任何入链,权重低、收录慢;
  • 导航栏与页脚是全局的链接出口,能让所有页面被顺利发现;
  • 面包屑既给用户看,也给爬虫一条语义路径;
  • 锚文本(链接文字)告诉爬虫目标页面是关于什么的,用「Next.js 渲染方式」比用「点击这里」有效得多。

canonical 与重复内容

同一个内容如果能通过多个 URL 访问(带参数的列表页、分页、带追踪参数的链接),搜索引擎需要知道哪个是权威版本:

<link rel="canonical" href="https://example.com/articles/koa-onion" />

缺了 canonical,权重会分散到多个 URL 上。站点地图里的 URL、页面里的 canonical、内部链接指向的地址,三者要一致。

robots.txt 与 noindex 是两件事

这是最常被搞混的一处:

指令 位置 含义
robots.txt 的 Disallow 站点根目录 不让抓取这个 URL
<meta name="robots" content="noindex"> 页面 <head> 抓到了,但别索引

危险组合:在 robots.txt 里屏蔽了某个页面,同时那个页面又有 noindex。爬虫按规则不会去抓它,于是永远看不到这条 noindex,反而可能把它的 URL 当作正常链接收录。要禁止收录又不给抓取权,就把 noindex 放在页面里,并且不要在 robots.txt 里屏蔽它。

Core Web Vitals 的阈值

Google 当前的三个核心网页指标阈值:

指标 含义 良好阈值
LCP 最大内容绘制,页面主体内容出现的时间 ≤ 2.5 秒
INP 交互到下次绘制,点击后的响应速度 ≤ 200 毫秒
CLS 累积布局偏移,页面元素有没有乱跳 ≤ 0.1

服务端渲染与预渲染主要改善的是 LCP 与 FCP(HTML 里就有内容,不用等 JS 画);INP 取决于交互逻辑与主线程是否被长任务阻塞,通常要靠代码分包、减少注水负担来解决。CLS 最常见的来源是图片没有预留尺寸、字体替换造成的重排。

五、四种方法自证爬虫到底看到了什么

别猜,去看。下面四种方法按「成本从低到高」排列,建议固定在发布流程里跑一遍。

方法 1:右键「查看网页源代码」

最基础也最有效。右键页面 → 查看网页源代码,在源码里搜正文里出现过的那句话。

  • 搜得到 → 正文是服务端或预渲染输出的,SEO 基础没问题;
  • 搜不到 → 无论浏览器里显示得多正常,这就是客户端渲染。

注意区分两个面板:开发者工具的元素面板显示的是 JavaScript 执行后的 DOM,它永远「看起来正常」,不能作为任何 SSR 证据;要看的只有「源代码」。

方法 2:用命令行直接抓原始 HTML

比肉眼点右键更可靠,也更适合放进自动化检查:

# 抓取服务端返回的原始 HTML,正文关键词应能命中
Invoke-WebRequest -Uri 'https://example.com/blog/koa-onion' -UseBasicParsing |
  Select-String -Pattern 'Koa 中间件实战' -Quiet

返回 True 说明正文在服务端 HTML 里;返回 False 就是空壳。

方法 3:禁用 JavaScript 后重新加载

在浏览器设置里禁用 JavaScript(或用开发者工具的 Network 面板把脚本请求全部 block),然后刷新页面。

  • 页面依然完整 → 爬虫看到的和你看到的一致;
  • 页面变空白 → 爬虫看到的也是空白。

方法 4:Google Search Console 的 URL 检查工具

这是以 Google 视角为准的最终依据:

  1. 在 Search Console 里用「URL 检查」输入页面地址;
  2. 点「测试实时网址」(Test Live URL);
  3. 看「已呈现的 HTML」(Rendered HTML)这一栏的截图和源码;
  4. 如果这里缺内容,说明 Google 拿到的版本和用户看到的不一致。

同一页面还可以对比「抓取的 HTML」与「已呈现的 HTML」两栏,差异就是客户端渲染造成的信息损失。

六、SSR 也会翻车:五个真实踩坑点

服务端渲染不是免死金牌,下面五种情况照样让爬虫读不到该读的。

1. 注水不匹配(hydration mismatch)导致内容被清空 服务端渲染的 HTML 与客户端首次渲染的结果不一致时,React 会丢弃服务端那份、用客户端结果覆盖。极端情况下页面直接白屏——对用户是 bug,对爬虫是「这页没内容」。

2. 元数据写在客户端组件里 把 title、description、OG 标签放进带 'use client' 的组件里,它们只会出现在渲染后的 DOM 里,第一波抓取拿不到。正确做法是在服务端段(page.tsx / layout.tsx)导出 metadata。

3. robots.txt 把 CSS 和 JS 挡掉了 有些站为了「防爬」屏蔽了静态资源目录。问题是渲染引擎需要 CSS 判断布局、需要 JS 执行代码,资源被挡后渲染出来的页面可能大不一样,甚至直接放弃。

4. 软 404 与客户端重定向 「页面不存在」时返回 200 加一段前端提示文案,爬虫会判为软 404——它以为这是个正常页面,只是内容为「没有这篇」,但并不据此丢掉整站。同理,页面加载后用 JS 跳到别处,爬虫看到的仍是原地址,容易把两个 URL 当成重复内容。

5. JS 包太大,SSR 只是换了个地方卡 HTML 是送到了,但注水要执行的 JS 有 2MB,用户和渲染引擎都得等。「服务器渲染 + 巨大客户端包」的实际首屏可能比纯 CSR 还差。

七、渲染方式怎么选:按页面职责分

不要用一个渲染方式套全站。绝大多数内容站的最优解是混合,按页面职责分开定:

页面类型 推荐渲染 理由
文章详情、文档、落地页 SSG / ISR 内容稳定,静态 HTML 对爬虫最友好,CDN 还能兜住流量
列表页、标签页 ISR 内容偶尔更新,定期重建即可,不必每次请求都算
商品详情、库存、报价 ISR 或 SSR 数据要相对新,但不需要毫秒级实时
搜索结果、筛选页 SSR 每次查询组合都不同,缓存没有意义
个人中心、后台面板 CSR 登录后才可见,本来就不需要被收录
需要个性化推荐位 SSR 优先 爬虫至少能读到主体内容

判断标准可以压缩成一句话:这个页面的内容需要被搜索到吗? 需要,就服务端渲染或构建期生成;不需要,CSR 完全够用,而且更简单。

八、实战:给一个内容站定一套渲染方案

前面讲的是原理,这一段把它落成一个具体站点。以「技术博客 + 文档站 + 后台」这类典型内容站为例:

页面 路由示例 渲染方式 为什么
首页 / 预渲染(SSG) 内容每天只变几次,构建时生成即可
文章详情 /articles/[id] ISR,周期 10 分钟 内容偶尔更新,要能被及时收录
标签 / 分类列表 /tags/[tag] ISR,周期 1 小时 组合多、变化慢
站内搜索结果 /search?q= 客户端取数或 SSR 不需要被收录,参数组合无限
文档页 /docs/** 预渲染(SSG) 版本化内容,随发布一起更新
登录后控制台 /dashboard/** 纯客户端(CSR) 登录后才可见,没有收录价值
订阅接口 /api/** 与前端无关 JSON 接口

对应的 Next.js 配置:

// app/articles/[id]/page.tsx —— 文章详情:ISR
export const revalidate = 600; // 10 分钟重建一次

export default async function ArticlePage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const article = await getArticle(id);
  return <Article article={article} />;
}
// app/dashboard/page.tsx —— 控制台:只在浏览器渲染
'use client';

export default function DashboardPage() {
  return <Dashboard />; // 数据在浏览器里取,服务端不参与
}
// app/search/page.tsx —— 搜索结果:客户端取数
'use client';

import { useState } from 'react';

export default function SearchPage() {
  const [keyword, setKeyword] = useState('');
  const [results, setResults] = useState<ArticleSummary[]>([]);

  async function search() {
    const res = await fetch(`/api/search?q=${encodeURIComponent(keyword)}`);
    setResults(await res.json());
  }

  return (
    <div>
      <input value={keyword} onChange={(e) => setKeyword(e.target.value)} />
      <button onClick={search}>搜索</button>
      <ul>{results.map((r) => <li key={r.id}>{r.title}</li>)}</ul>
    </div>
  );
}

上线后的检查节奏

  • 发版当天:用第四节四种方法过一遍关键页面(首页、一篇新文章、一个标签页);
  • 每周:在 Search Console 看「已编入索引」与「已发现但未编入索引」两个数字的走势;
  • 每月:抽查站点地图里的 URL 是否都能正常返回 200,canonical 是否与实际 URL 一致。

这套节奏的意义在于:渲染相关的故障是静默的。页面照常显示、接口照常返回,只有搜索结果悄悄不来。等流量少了才发现,通常已经过去几周。

九、发布前的检查清单

把这张表贴到发布流程里,比事后补救便宜得多:

  • 文章正文能在「查看网页源代码」里搜到(不是元素面板);
  • title、meta description、canonical 出现在服务端返回的 HTML 里;
  • OG 标签在服务端输出,社交分享卡片能正常显示;
  • 禁用 JS 后页面仍可读;
  • robots.txt 没有屏蔽 CSS / JS;
  • 不存在的文章返回真正的 404,而不是 200 + 空文案;
  • URL 变更时同步更新 canonical 与站点地图;
  • sitemap.xml 里包含这个页面(Next.js 里可用 app/sitemap.ts 自动生成);
  • 关键页面在 Search Console 的「已呈现的 HTML」里内容完整。

参考资料

标签

  • #SEO
  • #ssr
  • #csr
  • #rendering
  • #收录

相关课程