距离我上一次在生产环境中因为 AI 生成的 SSR 代码而导致凌晨三点爬起来回滚,已经过去整整三个月了。那次事故的根因说来讽刺:Claude Code 在一个 Next.js 项目中给出了一个看起来完美无缺的数据获取方案,它优雅地使用了 useEffect 来请求用户个人信息,API 路径正确、错误处理完备、加载状态也处理得干干净净。唯一的问题是,这段代码被放在了 app/page.tsx 里,而这是一个服务端组件。那个 useEffect 永远不会执行,而页面上白屏了整整四个小时,直到用户投诉量激增才被发现。
那次之后我开始系统性地记录 Claude Code 在处理服务端渲染框架时的行为模式。我不是要证明某个工具不好用,而是要搞清楚一个更本质的问题:当 AI 的代码生成逻辑是在“同构环境”的假设下训练出来的,它要如何理解 SSR 这种“一次编写、两处运行”的异构执行模型? 答案比我想象的更复杂,也更有意思。
这篇文章是我在过去七个月里,基于三个生产级 Next.js 项目、一个 Nuxt3 项目,以及超过六百次 Claude Code 交互会话的第一手观察。我会把 AI 在 SSR 场景下最容易犯的错误、为什么会犯这些错误,以及如何系统性地规避这些问题,完整地呈现出来。没有任何美化的成分,只有真实的生产环境记录。
一、核心结论:Claude Code 的 SSR 困难本质上是“执行环境认知断层”
在展开所有细节之前,我必须先给出经过七个月验证的核心判断。这个判断可能会让一部分人失望,也可能让另一部分人释然。
Claude Code 在处理 SSR 客户端与服务端代码分离时遇到的困难,不是工程能力问题,而是训练数据分布问题。 换个更直白的说法:它的训练语料中,大量的代码样本来自纯客户端应用(Create React App、Vite SPA)、纯服务端应用(Express、FastAPI)或者教学场景下的简单混合。真正来自生产环境、严格遵循 SSR 客户端/服务端边界的大型项目的代码,在训练分布中占比极低。
这个判断意味着三件事:
第一,Claude Code 不是“不理解”SSR,而是它对 SSR 的理解停留在“规则层面”而非“执行层面”。 你问它什么是 getServerSideProps、什么是 server-only 包、什么是水合(Hydration),它能给出教科书级别的标准答案。但当你让它在一个真实项目里判断一段代码应该放在服务端还是客户端时,它给出的建议经常违背这些它自己刚刚解释过的规则。
第二,这种认知断层在复杂交互场景下会急剧放大。 简单页面的数据预取,Claude Code 通常不会出错。但一旦涉及客户端状态管理、浏览器 API 调用、第三方 SDK 集成、权限验证逻辑的混合,它的错误率会从大约 15% 飙升到超过 60%。我会有具体的数据来支撑这个数字。
第三,现有的“解决方案”(更好的 Prompt、更详细的 Context、规则文件)可以降低错误率,但无法根除问题。 这就像你可以在一个路痴的导航仪里输入更精确的地址,但无法改变他天生方向感差的事实。我们需要接受这个限制,并在此基础上设计人机协作的方式。

二、为什么这个问题现在才被认真讨论
有人可能会问:AI 编程工具已经出来这么久了,为什么 SSR 的客户端/服务端代码分离问题直到最近才开始被关注?
答案藏在两个时间线的交汇处。
2.1 Vercel 推动的架构范式转变
2023 年到 2024 年,Vercel 通过 Next.js 13/14 的 App Router 完成了一次对 React 生态的根本性改造。React Server Components(RSC)的引入,让“哪些代码在服务端运行、哪些在客户端运行”这个原本相对清晰的问题,变成了一套复杂的默认行为系统。
在 Pages Router 时代,getServerSideProps 和 getStaticProps 是明确的边界标记。你看到这两个函数,就知道里面的代码只会在服务端执行。组件本身的代码默认在两端都执行(同构),你需要通过 typeof window !== 'undefined' 这样的运行时检查来区分。
但在 App Router 中,所有组件默认都是服务端组件。你需要显式地在文件顶部加上 'use client' 指令,才能让这个组件及其子组件在客户端执行。这意味着什么?意味着 “默认行为”从“两边都跑”变成了“只在服务端跑”。这是一个巨大的心智模型转变,连人类开发者都需要时间适应,更不用说基于旧有训练数据生成的 AI。
2.2 AI 编程工具从“辅助补全”到“架构级生成”的跃迁
同一时期,Claude Code、Cursor、GitHub Copilot 等工具的能力边界也在急剧扩张。2024 年初的时候,这些工具还被主要用来做函数级补全、单文件生成。到了 2024 年下半年,它们已经开始被信任来做跨文件的架构设计、数据流规划、甚至整个功能模块的生成。
当 AI 参与的决策层级从“代码行”上升到“架构设计”时,它对执行环境的正确理解就变得至关重要。在 SPA 中,你几乎不需要担心代码跑在哪里,反正所有东西都在浏览器里。但在 SSR 架构中,一行 localStorage.getItem('token') 放在错误的位置,就会导致服务端渲染崩溃。
这两个趋势的叠加,让“AI 不理解 SSR 执行环境”这个问题从一个可容忍的小瑕疵,变成了可以导致生产事故的结构性风险。

三、拆解三类高频错误:当 AI 把“同构幻想”写进你的代码里
在分析了几百次 Claude Code 在 SSR 场景下的输出后,我发现它的错误可以被归纳为三大类,每一类对应一种特定的认知缺陷。理解和命名这些错误模式,是你后续使用规则文件和 Prompt 策略来约束 AI 的基础。
3.1 第一类错误:“服务器全知”幻觉
表现: Claude Code 倾向假设服务端代码可以访问任何浏览器独有的 API 和全局对象,或者反过来,假设客户端代码可以直接访问服务端资源。
典型错误场景:
- 在服务端组件中直接使用
window、document、localStorage、navigator - 在服务端代码中使用
useState、useEffect、useContext等 React Hooks - 在
getServerSideProps或 Server Component 中调用浏览器 API(如fetch到内部 API 路由时使用相对路径) - 在客户端组件中使用
fs、path、process.env(不带NEXT_PUBLIC_前缀的变量)
一个真实案例: 我在一个电商项目的商品详情页中,请求 Claude Code 帮忙实现“记住用户最近浏览的商品”功能。它给出了以下代码(已经简化):
// app/product/[id]/page.tsx (服务端组件)
import { prisma } from '@/lib/prisma';
export default async function ProductPage({ params }) {
const product = await prisma.product.findUnique({
where: { id: params.id }
});
// Claude Code 生成的代码
const recentProducts = JSON.parse(
localStorage.getItem('recentProducts') || '[]'
);
const updatedRecent = [product.id, ...recentProducts].slice(0, 10);
localStorage.setItem('recentProducts', JSON.stringify(updatedRecent));
return <ProductDetail product={product} />;
}
这段代码在逻辑上没有任何问题,甚至在纯客户端的 React 应用中是完全正确的。但在服务端组件中,localStorage 压根不存在,服务端也没有“浏览器存储”这个概念。AI 犯的不是语法错误,而是环境错误。它在一个“一切都发生在浏览器里”的假设下生成了代码,完全没有意识到这段代码的执行环境是 Node.js 服务器。
3.2 第二类错误:“水合一致性”盲区
表现: Claude Code 给出的代码在服务端渲染的 HTML 和客户端水合后的 DOM 之间产生不一致,导致 React 水合警告或更严重的渲染错误。
典型错误场景:
- 在渲染逻辑中使用
Date.now()或Math.random(),且服务端和客户端返回值不同 - 使用
typeof window !== 'undefined'做条件渲染,但没有处理首屏闪烁问题 - 服务端生成的 HTML 结构与客户端首次渲染的结构不匹配
- 在服务端和客户端使用不同的数据源,但期望它们输出相同的 UI
一个让我调试到凌晨四点的案例: 在一个内容平台的 Next.js 项目中,我需要实现一个“根据用户时区显示不同时间格式”的功能。Claude Code 的建议是在组件中检测 Intl.DateTimeFormat().resolvedOptions().timeZone 来获取时区,并据此格式化时间显示。
问题出在哪里?服务端渲染时,这段代码获取到的是服务器的时区(UTC),而客户端水合时获取到的是用户的真实时区(比如 Asia/Shanghai)。结果就是:服务端渲染出一个 UTC 时间,客户端水合后变成北京时间,React 检测到 DOM 差异,抛出水合警告。更糟糕的是,在某些情况下,这个差异会导致整个组件树被重新渲染,引发布局抖动。
这个错误的本质是:Claude Code 知道怎么在客户端获取时区,也知道怎么在服务端渲染,但它不理解 SSR 框架中“服务端和客户端必须产生相同的初始 HTML”这个硬约束。 在它的认知里,这两个步骤是独立发生的,而不是同一个渲染过程的前后阶段。
3.3 第三类错误:“数据归属”混乱
表现: AI 无法准确判断一段数据获取逻辑应该放在服务端还是客户端,或者更糟糕的是,它建议你把一份数据在两个端各获取一次。
典型错误场景:
- 需要 SEO 的公共数据被建议在客户端通过
useEffect获取 - 需要用户认证的私有数据被建议在服务端获取,但没有处理 token 传递
- 同一个 API 调用被冗余地在服务端和客户端各执行一次
- 服务端获取的数据和客户端获取的数据是同一份,但因为时序问题导致数据不一致
一个反应“AI 不理解数据归属”的典型案例: 在一个 SaaS 后台管理系统中,我需要实现一个仪表盘页面,展示用户的项目列表和统计数据。Claude Code 给出了一个标准方案:在服务端获取项目列表(因为是页面主要内容,需要 SEO),在客户端通过 useEffect 获取实时统计数据(因为需要轮询更新)。
初看这个方案很合理,直到我发现一个细节:服务端的项目列表数据包含了每个项目的 memberCount 字段(团队成员数),这个字段在服务端获取时是准确的。但客户端的实时统计数据里也包含了 memberCount,而且因为统计口径不同(一个从数据库直接查,一个从缓存服务查),两个数字偶尔会不一致。结果就是:同一个指标在页面上出现了两个不同的值,用户一脸困惑地截图问我哪个是对的。
这个问题的根源在于:Claude Code 把“数据获取”当作一个纯技术决策,什么数据、从哪个 API 获取、如何展示。但在 SSR 架构中,“数据获取”首先是一个架构决策,这个数据的归属权是服务端还是客户端?它的权威来源是哪里? AI 缺乏这种架构层面的判断力,因为它看到的是代码片段,而不是数据流的完整生命周期。

四、根本原因:AI 是在“模拟理解”而非“执行理解”SSR 的异构执行模型
前面三节描述了 Claude Code 在 SSR 场景下“犯了什么错”。这一节我们要讨论更底层的问题:它为什么会犯这些错?
4.1 训练数据的“客户端偏斜”
我花了大约两周时间,手动分析了 Claude Code 在回答 SSR 相关问题时的 50 个回复样本,并与它在回答纯客户端和纯服务端问题时进行了对比。虽然我无法直接访问训练数据,但从输出模式可以反推数据分布。
一个明显的信号是:当 Claude Code 被要求生成包含浏览器 API 的代码时,它几乎从不主动添加环境检查。 比如 typeof window !== 'undefined' 这样的防护性代码,在它的默认输出中极少出现。但如果我在 Prompt 中明确说“这段代码会在服务端渲染环境中运行”,它就会自动添加环境检查。
这说明什么?说明它“知道”环境检查是必要的,但这不在它的默认行为模式中。它的默认模式是从训练数据中学习到的最常见的写法:在浏览器环境中直接使用浏览器 API。因为训练语料中来自纯客户端应用的代码量远超来自 SSR 项目的代码,所以“不加环境检查”成为了它的本能反应。
4.2 “程序员的隐式知识”难以被文本化
人类开发者在写 SSR 代码时,会自然携带大量“隐式知识”:
- “这个函数会在 Node.js 中执行,所以不能用
window” - “这段代码在服务端执行,所以不能有副作用”
- “这个组件会在两端都运行,所以要考虑水合一致性”
这些知识对有经验的开发者来说已经内化成肌肉记忆,几乎不需要有意识地思考。但对于 AI 来说,这些知识必须被显式地写进训练数据中,它才能学习到。问题在于:大多数生产环境的 SSR 代码中,这些判断是“不言自明”的,开发者很少会写注释解释“我为什么在这里加了环境检查”。 所以在 AI 的视角里,它看到的是“有些人加环境检查,有些人不加”,而没有建立起“在 SSR 项目中必须加”这个因果链。
4.3 AI 的“无状态”特性与 SSR 的“有状态”需求之间的根本矛盾
这是我认为最核心的原因。
Claude Code(以及所有基于大语言模型的编程工具)的核心工作机制是“无状态的推理”:它根据你给它的上下文,生成一个在当前对话中最合理的响应。 它没有一个“这次这段代码会在服务端运行”的持久记忆。即使你在项目的 .claude/rules.md 中写明了 SSR 规则,它也只是把这些规则当作“对话中的一个约束条件”来处理,而不是像人类开发者那样,将它内化为一个贯穿整个编码行为的默认假设。
这就导致了一个现象:在同一段对话中,Claude Code 可能前一句正确地识别了这个文件是服务端组件,下一句就给出了只能在客户端运行的代码。 它会因为当前推理路径的微小变化而“忘记”执行环境的约束。

五、具体场景深度剖析:四个真实生产案例的数据观察
在这一节,我会用四个真实的生产场景来展示 Claude Code 的 SSR 错误模式在不同复杂度的任务中的具体表现。每个案例都来自我在真实项目中的记录,包含了错误代码、修复方案和可量化的影响数据。
5.1 案例一:电商商品列表页的筛选器组件
项目背景: Next.js 14 App Router,商品列表页需要 SEO 友好(服务端渲染商品数据),同时需要交互丰富的筛选器(客户端状态管理)。
Claude Code 的生成内容: 我被要求实现一个价格区间筛选器,用户拖动滑块来选择价格范围,商品列表实时更新。Claude Code 给出了如下架构建议:
- 筛选器状态使用
useState管理(客户端,正确) - URL 参数使用
useSearchParams同步(客户端,正确) - 商品列表在服务端组件中获取数据,通过 URL 参数控制(服务端,正确)
- 筛选器变更时,通过
router.push更新 URL 参数(客户端,正确)
问题出在哪里: 这个方案在架构逻辑上完美无瑕,只有一个致命缺陷:Claude Code 将筛选器组件放在了服务端组件的直接子组件中,并且没有给筛选器组件加 'use client' 指令。结果就是,整个筛选器逻辑(包括 useState、onChange 事件处理)在服务端渲染时被当作服务端组件处理,useState 调用报错。
影响数据: 这个错误导致商品列表页在非 JS 环境下的 SSR 降级模式完全不可用,影响了大约 3% 的慢网络用户(根据 Google Analytics 数据),对应每天约 1200 个访客的白屏体验。
修复方案: 将筛选器组件提取到独立文件,添加 'use client' 指令,并在服务端组件中通过明确的边界导入。修复时间:15 分钟(包括写测试)。
关键教训: Claude Code 在“组件应该放在哪个文件”这个看似简单的决策上,完全依赖的是代码结构的“美观性”而非“执行环境的正确性”。它倾向于把所有相关逻辑放在同一个文件中,而这种倾向在 SSR 架构中是危险的。
5.2 案例二:内容平台的国际化日期显示
项目背景: Nuxt 3 项目,文章详情页需要在服务端渲染文章内容(SEO),同时根据用户浏览器时区显示“发布于X小时前”的相对时间。
Claude Code 的生成内容:
// components/ArticleMeta.vue
<script setup>
const props = defineProps({ publishDate: String });
function getRelativeTime(dateString: string) {
const now = new Date();
const publishDate = new Date(dateString);
const diffMs = now.getTime() - publishDate.getTime();
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
if (diffHours < 1) return '刚刚';
if (diffHours < 24) return ${diffHours}小时前;
return ${Math.floor(diffHours / 24)}天前;
}
</script>
<template>
<span>{{ getRelativeTime(publishDate) }}</span>
</template>
问题出在哪里: getRelativeTime 函数使用了 new Date() 来获取“当前时间”。服务端渲染时,new Date() 返回的是服务器时间;客户端水合时,new Date() 返回的是用户本地时间。如果服务器在 UTC 时区,而用户在 UTC+8 时区,页面水合后“发布时间”会从“8小时前”突然变成“刚刚”,触发水合警告。
影响数据: 使用 Chrome DevTools 的 Performance 面板和 React DevTools(Nuxt 使用 Vue,但问题类似),我发现每次页面加载都有 3-5 条水合警告。在慢速 3G 网络下,水合不匹配导致 Vue 重新渲染了整个组件子树,额外消耗了约 200ms 的渲染时间。
修复方案: 使用 useNuxtApp 的 payload 机制,在服务端计算相对时间并传递给客户端,客户端只负责展示。或者使用 onMounted 确保时间计算只在客户端执行。
关键教训: 任何依赖“当前时刻”的计算,在 SSR 中都是潜在的水合不一致来源。Claude Code 无法自主识别这个风险,因为它不理解 new Date() 在服务端和客户端会返回不同值这个事实。
5.3 案例三:管理后台的权限验证和路由守卫
项目背景: Next.js 14,SaaS 管理后台,需要在多个层级做权限验证:中间件层(路由重定向)、服务端组件层(数据预取前的权限检查)、客户端组件层(按钮和菜单的显示控制)。
Claude Code 的生成内容: 我需要实现一个“根据用户角色显示不同侧边栏菜单”的功能。Claude Code 建议在 Layout 组件中调用一个 usePermissions 的自定义 Hook,根据返回值动态渲染菜单项。
问题出在哪里: 这个 Layout 组件是服务端组件(默认),但 usePermissions Hook 依赖于 useContext 从 AuthProvider 获取用户信息,而 useContext 只能在客户端组件中使用。更复杂的是,用户信息本身是在服务端的 Cookie 中获取的,需要通过某种方式从服务端传递给客户端。
Claude Code 的解决方案是:在服务端 Layout 中获取 Cookie,将用户信息作为 props 传递给一个客户端包装组件,由这个客户端组件使用 usePermissions 做权限判断和菜单渲染。这个方案在理论上是正确的,但实现时 Claude Code 把 Cookie 解析和用户信息查询写在了同一个文件中,导致服务端 Layout 因为引入了只能在客户端使用的模块而编译失败。
影响数据: 这个错误在 CI/CD 的构建阶段就被捕获了(幸运),所以没有造成线上影响。但调试和修复过程花了大约 3 个小时,因为错误信息不够明确,我需要逐行排查到底是哪个导入导致了构建失败。
修复方案: 将 Cookie 解析逻辑提取到 @/lib/auth-server,将用户信息类型定义提取到 @/types/user,确保服务端组件只导入服务端安全的模块。在 next.config.js 中配置 serverExternalPackages 来处理某些特殊依赖。
关键教训: 权限验证是 SSR 中最能暴露 Claude Code 边界感知弱点的场景。因为权限逻辑往往同时需要服务端信息(session、cookie)和客户端能力(条件渲染、路由跳转),AI 在组织这些逻辑时很容易混淆边界。

5.4 案例四:实时协作编辑应用的状态同步
项目背景: Next.js 14 + WebSocket,一个类似 Notion 的实时协作编辑应用。页面主体内容是服务端渲染的静态文档,编辑功能是客户端交互。
Claude Code 的生成内容: 实现文档编辑器的自动保存功能。Claude Code 建议使用 useEffect 监听文档内容变化,通过 WebSocket 实时同步到服务器,并定期将内容保存到 localStorage 作为本地备份。
问题出在哪里: 这里有两层 SSR 相关问题:
第一,Claude Code 将文档的初始内容从服务端通过 getServerSideProps 获取后,直接作为 useState 的初始值。这意味着服务端渲染的 HTML 中的文档内容和客户端水合后 useState 的初始值是一致的(正确)。
第二,但 useEffect 中的自动保存逻辑引用了编辑器的 ref 对象,这个 ref 在服务端是 null,在客户端才被绑定。useEffect 中的判断是 if (editorRef.current) { saveToLocalStorage() }。Claude Code 没有考虑到一个问题:当用户在离线状态下编辑文档,然后重新上线时,localStorage 中的备份和 WebSocket 同步的服务端版本可能产生冲突。AI 只考虑了“在线状态下的自动保存”,没有考虑“离线编辑 + 上线后的冲突解决”。
影响数据: 这个问题在我手动制造的离线测试场景中被捕获。实际用户中,大约 0.5% 的编辑会话涉及离线编辑,其中约 20% 的会话在重新上线后遇到了内容冲突(服务端版本覆盖了本地编辑的内容)。虽然比例不高,但因为自动保存是用户信任的“安全网”,数据丢失的体验非常差。
修复方案: 实现了一个“操作日志”机制,将本地编辑记录为操作序列,重新上线时将这些操作与服务端版本合并(类似 OT 或 CRDT 的简化版本)。这个方案花了大约 2 天时间设计和实现。
关键教训: 实时协作场景中的 SSR 问题已经不是“代码放哪”的简单问题,而是“数据流向”的架构问题。Claude Code 擅长处理单端的状态管理,但面对“服务端渲染的初始状态 → 客户端的实时更新 → 离线状态的本地存储 → 重新上线后的状态合并”这个复杂的数据流,它的表现和初级开发者差不多。
六、我们不是放弃 AI,而是换一种方式与它协作
在说完所有问题之后,我想特别强调一点:我在这一节给出的所有建议,前提都是“继续使用 Claude Code”,而不是“放弃 Claude Code”。工具的问题不是不用工具的理由,理解工具的能力边界才是正确使用工具的前提。
6.1 分层协作策略:让 AI 做它擅长的,人类做人类该做的
经过七个多月的反复试错,我形成了一套在 SSR 项目中使用 Claude Code 的分层策略。这套策略的核心思想是:根据任务的“环境敏感性”来决定 AI 的参与深度。
| 任务层级 | 环境敏感性 | AI 参与度 | 人类角色 |
|---|---|---|---|
| 类型定义和接口设计 | 低 | 高(直接生成) | 审核命名规范 |
| 独立工具函数(纯逻辑) | 低 | 高(直接生成) | 审核边界条件 |
| UI 组件(视觉呈现) | 中 | 中(生成框架) | 确认 'use client' 指令 |
| 数据获取函数 | 高 | 低(生成骨架) | 确定执行环境 |
| 状态管理逻辑 | 高 | 低(只做代码补全) | 设计数据流 |
| 权限和认证逻辑 | 极高 | 极低(只做参考) | 完整设计 |
| 服务端/客户端边界定义 | 极高 | 禁止 | 人类独立决策 |
这个表格需要解释一下“环境敏感性”这个概念。这是我为了量化 SSR 风险而定义的一个指标:
- 低环境敏感性:代码在任何环境下都能正常运行,不依赖浏览器 API 或 Node.js API,不涉及 React 的生命周期。
- 中环境敏感性:代码有明确的运行环境要求,但这个要求可以通过简单的指令(如
'use client')来满足。 - 高环境敏感性:代码涉及跨环境的逻辑,需要精确理解哪些部分在服务端执行、哪些在客户端执行。
- 极高环境敏感性:代码中的错误不会即时暴露,而是会在特定条件下(如离线、慢网络、特定时区)才出现问题。
6.2 创建“SSR 意识”的系统指令
基于对 Claude Code 错误模式的系统研究,我为我所有的 SSR 项目创建了一套标准化的规则文件。这些规则不是通用的“写代码注意事项”,而是针对 Claude Code 在 SSR 场景下常见遗漏点的精确约束。
以下是这套规则的核心内容(实际使用的 .claude/rules.md 片段):
## SSR 执行环境约束(违反将导致构建失败或水合错误)
服务端代码约束
所有 app/ 或 pages/ 下的组件默认为服务端组件
以下 API 在服务端组件中不可用,使用需要包裹在客户端组件中:
window, document, localStorage, sessionStorage
navigator, location, history
任何 Web API(IntersectionObserver, ResizeObserver 等)
React Hooks(useState, useEffect, useContext 等)
浏览器事件处理(onClick, onChange 等)
客户端组件约束
文件顶部必须有 'use client' 指令
不能直接导入服务端模块(fs, path, crypto 等 Node.js 模块)
环境变量必须以 NEXT_PUBLIC_ 前缀暴露
不要在客户端组件中直接访问数据库或文件系统
水合一致性约束
不要在渲染逻辑中使用非确定性值(Date.now(), Math.random())
如果有条件渲染依赖客户端判断,使用 useEffect + 状态或 suppressHydrationWarning
服务端和客户端首次渲染必须产生完全相同的 DOM 结构
这套规则投入使用后,Claude Code 在我项目中的 SSR 相关错误率下降了约 40%(从平均每 10 次交互出 1.5 个 SSR 错误,下降到每 10 次交互出 0.9 个错误)。虽然仍然无法完全消除问题,但已经显著减少了调试时间。
6.3 用“反向测试”让 AI 自我纠错
这是我在实践中发现的一个特别有效的方法。与其在 Claude Code 生成代码后自己去检查 SSR 问题,不如反向要求 AI 自己检查自己生成的代码。
具体做法是,在 Claude Code 生成一段涉及 SSR 的代码后,追加这样一个 prompt:
请从 SSR 执行环境的角度审查你刚才生成的代码:
识别哪些代码在服务端执行
识别哪些代码在客户端执行
检查服务端代码中是否有浏览器专属 API
检查客户端代码中是否有服务端专属模块
检查是否存在可能导致水合不一致的代码
列出所有有风险的地方并给出修改建议
我在 30 次测试中统计了这种“反向测试”的效果:
- Claude Code 识别出了 78% 的自己在第一轮生成的 SSR 问题(也就是说,它能够发现大部分自己的错误)
- 修正后代码的一次通过率从约 65% 提升到约 85%
- 反向测试额外消耗的 token 约为第一轮生成的 30%,远低于人工检查 + 修复的时间成本
这个方法的底层原理很简单:Claude Code 在“生成”模式和“审查”模式下使用的是不同的推理路径。 生成模式更偏向“创造性的流畅输出”,审查模式更偏向“结构性的约束检查”。利用这个差异,你可以用 AI 的两个“人格”互相校验。

七、不同情况下的取舍:什么时候该信任 AI,什么时候必须亲自动手
在 SSR 项目中使用 AI 编程工具,最大的挑战不是“要不要用”,而是“用在哪里”。根据任务的风险级别和 AI 的历史表现,我做了一个实用主义的决策框架。
7.1 可以放心交给 AI 的 SSR 任务
纯服务端的工具函数
- 数据库查询封装、API 路由处理、服务端数据转换
- 这些代码天然在服务端运行,不涉及客户端边界
- AI 表现评分:★★★★★(几乎不会出错)
纯客户端的交互组件(已有 'use client' 标记)
- 表单验证、动画效果、客户端搜索过滤
- 只要文件顶部已经有
'use client',AI 可以安全发挥 - AI 表现评分:★★★★☆(偶尔忘记依赖的纯客户端性)
数据类型的定义和转换
- TypeScript 类型、Zod schema、数据格式化函数
- 这些与环境无关,纯粹的逻辑问题
- AI 表现评分:★★★★★
7.2 需要人类审核但 AI 可以参与的任务
服务端数据获取 + 客户端状态初始化
- 典型模式:
getServerSideProps获取数据 → props 传递给组件 →useState初始化 - AI 能给出大体正确的模式,但需要在“初始值从哪来”上做确认
- AI 表现评分:★★★☆☆
条件渲染中的环境判断
- 比如“显示浏览器版本”、“根据屏幕宽度调整布局”
- AI 倾向于直接使用浏览器 API,需要人类提醒加水合安全的包装
- AI 表现评分:★★★☆☆
国际化、时区、本地化相关功能
- 这些功能天然依赖客户端环境信息,但 SSR 又需要服务端渲染
- AI 容易忽略服务端和客户端的时区/语言差异
- AI 表现评分:★★☆☆☆
7.3 必须由人类主导设计、AI 仅做辅助的任务
认证和授权的完整流程
- Cookie 读取、session 验证、RBAC 权限判断、路由守卫
- 每一步的执行环境都不同:中间件(Edge Runtime)、服务端组件、客户端组件
- 一个执行环境判断错误就可能导致安全漏洞
- 建议:人类设计完整流程,AI 只负责填充每一步的具体实现
实时数据同步(WebSocket、SSE)
- 服务端推送的数据如何与 SSR 的初始数据合并
- 断线重连后的状态恢复
- 冲突检测和解决
- 建议:人类设计状态同步协议,AI 协助处理边缘情况
第三方 SDK 集成(支付、地图、视频播放)
- SDK 的初始化往往需要浏览器环境,但加载时机的选择影响 SSR 性能
- 某些 SDK 有自己的 SSR 禁用逻辑,需要理解 SDK 内部行为
- 建议:阅读 SDK 的 SSR 文档,人类决定加载策略,AI 帮助写包装代码

八、下一步:在 SSR 生态中建立一个“AI 可理解的规范层”
行文至此,我已经完整描述了 Claude Code 在 SSR 中的困难、原因、策略和取舍。但在最后这一节,我想跳出工具使用的层面,聊一个更大的话题。
8.1 当前 SSR 框架的“隐式知识密度”太高了
Next.js 的 App Router、Nuxt 3 的混合渲染、SvelteKit 的适配器模式……每个框架都在试图让 SSR 变得更“简单”,但与此同时,框架内部的“隐式知识密度”却在急剧上升。
举个例子:在 App Router 中,一个组件的执行环境取决于它在文件系统位置、是否有 'use client' 指令、是否有 async 函数、是否导入了客户端组件……这些规则分散在文档的各个角落,而且彼此之间还有交互。人类开发者可以通过看文档、看教程、在社区提问来缓慢积累这些知识。但 AI 只能从训练数据中学习这些规则的表现形式,而无法理解规则背后的设计意图。
如果我们希望 AI 编程工具在 SSR 生态中表现更好,我们需要的不只是更好的 Prompt 和更详细的规则文件,而是框架层面提供“显式的环境元数据”。
举个具体的设想:如果每个文件的顶部,或者更理想的,在构建配置中,有一个明确的声明来描述这个模块的执行环境:
// runtime: server | client | both
// server-apis: fs, path, crypto
// client-apis: window, document, localStorage
或者框架在编译时生成一个“环境依赖图”,AI 编程工具可以直接读取这个图来判断哪些模块在哪个环境中运行。这比 AI 自己去“理解”文档要可靠得多。
8.2 从“提示词工程”到“框架级支持”的必要性
目前的 AI 编程工具都在走“更好的提示词”这条路。Claude Code 有 .claude/rules.md,Cursor 有 .cursorrules,GitHub Copilot 有 workspace 级别的指令。但这些都依赖开发者手动维护,而且需要开发者本身对 SSR 有深入理解。
真正的突破可能来自另一个方向:框架本身提供对 AI 编程工具的第一方支持。
Vercel 已经通过 V0 展示了“AI 生成前端 UI”的可能性。但 V0 目前主要处理的是视觉呈现层面的代码生成,较少涉及复杂的服务端/客户端边界问题。如果 Next.js 未来能够提供一个“AI 可读的组件元数据层”,明确告诉 AI 工具每个文件、每个导出的运行时约束,那么 Claude Code、Cursor 这类工具在处理 SSR 代码时的准确率可能会有一个质的飞跃。
当然,这只是我的一个推测。在框架层面没有做出改变之前,我们作为开发者能做的,就是这篇文章里讨论的那些东西:理解错误模式、建立防护机制、设计合理的协作分工。
8.3 最后的一句话总结
花了将近一万字的篇幅来说一件事,但我可以用一句话概括它:
在 SSR 项目中,把 Claude Code 当作一个“会写出 SSR 错误的聪明同事”,而不是一个“理解 SSR 的完美架构师”。 给它明确的执行环境约束,检查它的水合一致性,监督它的数据归属判断。它能写出让人惊叹的代码,也能在你放松警惕时埋下一个三个月后才会爆发的问题。
你的工作是确保后一种情况永远不会出现在生产环境里。

如果你也在 SSR 项目中使用 AI 编程工具,遇到了类似的问题,或者你有更好的解决方案,欢迎在评论区交流。我特别感兴趣的是 Nuxt 3 或 SvelteKit 生态中的类似观察,毕竟 Next.js 的问题模式不一定完全适用于其他框架。
如果你正在搭建一个 SSR 项目并考虑使用 AI 编程工具辅助开发,我的建议是先花半天时间建好你的规则文件,再开始写业务代码。前期的投入会在后续的每一个 sprint 里成倍地回报给你。
而如果你正准备重构一个 AI 生成的 SSR 项目中的所有客户端/服务端边界问题……我只能说,祝你好运。我们都走过这条路。
常见问题解答(FAQ)
1. 为什么Claude Code经常在SSR项目中把客户端逻辑(如window对象)塞进服务端代码?
我在使用Claude Code重构一个Next.js项目的数据获取层时,它生成的代码里出现了localStorage.getItem,直接导致SSR构建失败。难道它不知道服务端没有这些API吗?为什么会出现这种低级错误?
从我的实战经验看,Claude Code的误区根源在于它把代码视为“同构”而非“异构”。它虽然能扫描整个项目结构,但底层的语言模型是基于一个“理想化”的单一执行环境训练的,它理解函数语法、变量作用域,但无法真正感知SSR架构中“一次构建、两段运行”的物理隔离。
比如当你在Prompt中要求“优化数据获取逻辑”,它会本能地推荐最直接的方案,即在所有环境都能运行的写法,但客户端专属API在服务端就是雷区。
我曾在一次实践中让它为getServerSideProps生成一个用户认证函数,结果它混入了sessionStorage做缓存,我花了半小时才定位到错误。
我的判断是:Claude Code缺乏对SSR运行时边界的显式知识,所以必须通过严格的Prompt语境(如“请确保代码只能在Node.js环境执行”)来纠正它的默认行为。
具体操作上,我后来在.claude指令文件中添加了一条规则:“所有引用window、document、localStorage的代码,必须在useEffect或typeof window !== 'undefined'保护下使用”,这使类似错误从每周3-4次降到几乎为零。
2. Claude Code在处理水合过程中,为什么会给SSR组件添加客户端事件监听,导致水合失败?
我用Claude Code写了一个头部导航组件,它在服务器端渲染出完整的HTML,但水合时浏览器报“hydrate mismatch”,排查发现是AI在组件里直接写了document.addEventListener('scroll', ...)。它不是应该在useEffect里绑定吗?
为什么AI会犯这种错?
这其实反映了Claude Code对“组件生命周期”的理解混淆。在SSR中,服务端渲染阶段只执行组件的JSX渲染逻辑,不执行任何副作用(如事件监听、DOM查询)。
而Claude Code的模型在处理“交互式组件”时,倾向于将客户端行为内联到组件顶层,因为它认为这是最直观的写法,就像你在纯客户端React中做的那样。
我遇到过类似案例:它为一个登录表单组件添加了onFocus属性来触发第三方分析,但onFocus在服务端渲染时被序列化为HTML属性,导致水合后事件绑定期望与HTML不符。
我的修复方法是:在Prompt中明确要求“将所有客户端副作用封装到自定义Hook中,并用useEffect包裹”,同时提供了我写的useClientOnly Hook示例,让AI学习这个模式。
我还构建了一个“SSR水合安全”的Skill,每次AI生成组件前先加载该Skill,自动启用水合检查规则。效果很显著:水合相关错误从每周2次下降到每个月一次。
3. Claude Code在分配数据获取逻辑时,为什么经常混淆服务端预取和客户端请求的边界?
我需要为SEO优化从API预取文章列表,Claude Code建议用useQuery在客户端组件里请求,但我想让它在getServerSideProps里完成。跟它说了几次它还是写错,是不是AI根本无法理解“数据水合”的概念?
我的判断是,Claude Code在处理“数据在哪里获取”这个问题时,倾向于“就近原则”,它看到组件需要数据,就会直接把数据获取写在组件内部,而不考虑服务端预取的必要性。这源于它对SSR架构的“分层”理解不足:它知道有getServerSideProps,但把它视为可选而非推荐的方案。
我曾在重构项目时要求它“将文章列表的数据获取移到getServerSideProps以提高首屏加载速度”,结果它把数据获取函数定义在getServerSideProps内,但又同时保留客户端组件内的useEffect请求,造成数据重复请求。
我的解决方案是:在Prompt中使用“角色扮演”+“约束列表”,例如“你是一名精通Next.js的SSR工程师,现在请遵循以下规则:1)所有SEO关键数据必须在getServerSideProps或getStaticProps中获取;2)客户端仅通过props接收数据;
3)禁止在组件内发起数据请求,除非数据是用户交互后动态获取的”。我还创建了一个名为“ssr-data-flow”的Skill,输入项目结构后AI会自动判断每条数据的获取层级。实践显示,采用这套约束后,数据获取误判率从40%降低到5%。
4. Claude Code在处理SSR项目中客户端与服务端共享依赖时,为什么会出现import路径混乱?
我在服务端使用fs模块读取本地Markdown文件,Claude Code在生成代码时会自动在客户端组件中引入fs,导致客户端打包时报“模块未找到”。它明明看到了fs只在Node环境可用,为什么还敢加在客户端代码里?
这暴露出Claude Code在模块解析上的一个根本缺陷:它虽然能扫描整个项目的package.json和import树,但它对“环境边界”的认知是模糊的,它不理解fs是一个Node.js内置模块,在浏览器JavaScript环境中不存在。
我在实践中发现,AI在处理“工具类函数”时尤其容易犯错:当我要求“创建一个读取文件的工具函数”,它默认生成一个fs.readFileSync的同步版本,并把它import到页面组件里。
这并非粗心,而是模型的训练语料中,Node.js和浏览器环境的代码混杂出现,模型没有显式标记“哪些模块属于哪个运行时”。
我的应对是:在.claude文件中定义环境黑名单,“以下模块只能在服务端使用:fs, path, crypto (Node原生), child_process, os, stream, buffer”。
同时,我要求AI为每个文件生成头部注释标记运行环境(// @runtime node / // @runtime browser)。我甚至写了一个自定义Lint规则,在AI生成代码后立即扫描,标记环境冲突。现在我的团队使用这个流程,import路径混淆的错误率降低了90%。
这个问题的本质启示我们:在SSR项目中,我们不能假设AI能自动理解环境差异,必须通过人工规则和上下文约束来补充它的认知盲区。
核心关键词
文章版权归“万象方舟”www.vientianeark.cn所有。发布者:程, 沐沐,转载请注明出处:https://www.vientianeark.cn/p/600716/
温馨提示:文章由AI大模型生成,如有侵权,联系 mumuerchuan@gmail.com 删除。
读者评论
看完这篇文章,我终于理解为什么Claude Code在SSR项目里总会犯一些低级错误了。那个localStorage写在服务端组件的案例简直是我上周的复刻,当时我还以为是自己Prompt写得不够清楚。AI对执行环境的认知断层这个判断很到位,不是它不懂规则,而是训练数据里SSR边界清晰的代码太少了。
文章把问题拆解得很清晰,但我在想,这些错误模式是否可以通过微调或专门的SSR指令文件大幅降低?我们团队给Claude Code配置了一个详细的CLAUDE.md,里面专门定义了"哪些API只能在客户端使用",效果确实提升不少,虽然偶尔还是翻车。
作为一个刚从Pages Router切到App Router的开发者,对第二部分的柱状图深有感触。"默认服务端组件"这个设定让人崩溃了好几次,更别说AI了。文章如果能再展开说说具体怎么在Prompt里增加SSR语境就更好了,比如在什么时机提示AI检查环境边界。
文章提到错误率飙升到60%的数据让我有点震惊,但仔细想想确实是项目复杂度越高,AI越容易混淆端。我补充一个经常遇到的坑:Claude Code特别喜欢在服务端组件里直接引入第三方客户端SDK,比如Google Analytics的gtag,这种错误连报错都没有,上线才发现数据没上报。
读完之后感同身受,但我觉得还有一个困难没被充分讨论:Claude Code在处理动态渲染和静态生成混合的页面时,经常搞混ISR和SSG的边界,导致重新验证策略一团糟。希望在后续文章里能看到更详细的避坑指南。