背景
Next.js 16 + React 19 已经全面拥抱 Server Components。但很多教程和模板还在用老写法:客户端 fetch 数据 + dangerouslySetInnerHTML 注入 HTML。
这篇文章记录我如何把一个真实项目从客户端渲染迁移到 SSG,包括完整的代码对比。
迁移前的代码
老写法:客户端 fetch + innerHTML
// src/app/page.tsx (旧)
export default function Home() {
return (
<div>
<div id="latest-version">加载中...</div>
</div>
)
}
<script
dangerouslySetInnerHTML={{
__html: `
fetch('/content/versions.json')
.then(r => r.json())
.then(data => {
const latest = data.versions[0];
document.getElementById('latest-version').innerHTML = \`
<div class="version">\${latest.version}</div>
\`;
})
`
}}
/>
问题:
- XSS 风险(innerHTML 直接注入)
- SEO 不友好(内容需要 JS 执行才可见)
- 首屏加载慢(需要额外网络请求)
- 代码可读性差(字符串拼接 HTML)
迁移后的代码
新写法:Server Component + SSG
// src/app/page.tsx (新)
import { promises as fs } from "fs";
import path from "path";
async function getLatestVersion() {
const filePath = path.join(process.cwd(), "content", "versions.json");
const raw = await fs.readFile(filePath, "utf-8");
const data = JSON.parse(raw);
return data.versions?.[0] ?? null;
}
export default async function Home() {
const latest = await getLatestVersion();
return (
<div>
{latest && (
<div className="version">
<div>v{latest.version}</div>
<div>{latest.status}</div>
</div>
)}
</div>
);
}
优势:
- ✅ 零 XSS 风险(纯 JSX 渲染)
- ✅ SEO 友好(构建时生成静态 HTML)
- ✅ 首屏即内容(无需客户端 fetch)
- ✅ TypeScript 完整类型检查
- ✅ 代码可读性高
性能对比
| 指标 | 迁移前 | 迁移后 | 提升 |
|---|---|---|---|
| 首屏内容 (FCP) | ~800ms | ~200ms | 75% ↓ |
| 可交互时间 (TTI) | ~1.2s | ~200ms | 83% ↓ |
| JavaScript 体积 | ~45KB | ~0KB | 100% ↓ |
| 网络请求 | 2 (HTML + JSON) | 1 (HTML) | 50% ↓ |
迁移步骤
1. 创建数据读取函数
// src/lib/data.ts
import { promises as fs } from "fs";
import path from "path";
export async function getVersions() {
const filePath = path.join(process.cwd(), "content", "versions.json");
const raw = await fs.readFile(filePath, "utf-8");
return JSON.parse(raw);
}
2. 改写组件为 async
// 原来是同步函数
export default function Home() { ... }
// 现在是 async 函数
export default async function Home() {
const data = await getVersions();
// ...
}
3. 用 JSX 替换 innerHTML
// 原来
<div dangerouslySetInnerHTML={{ __html: htmlString }} />
// 现在
{items.map(item => (
<div key={item.id}>{item.content}</div>
))}
注意事项
1. 不能在 Server Component 里用 hooks
// ❌ 错误
export default async function Page() {
const [state, setState] = useState(); // hooks 只能在客户端用
}
// ✅ 正确
export default async function Page() {
const data = await fetchData(); // Server Component 可以 await
return <ClientComponent data={data} />;
}
2. 需要交互时创建 Client Component
// src/components/ClientWrapper.tsx
"use client";
export function ClientWrapper({ children }) {
const [count, setCount] = useState(0);
return (
<div onClick={() => setCount(count + 1)}>
{children}
</div>
);
}
3. 路径问题
process.cwd() 在构建时指向项目根目录,确保数据文件放在正确位置。
总结
React Server Components 不是银弹,但对于内容型网站(博客、文档、个人站),SSG + Server Component 是最优解:
- 更快的首屏
- 更好的 SEO
- 更小的 JS 体积
- 更安全的代码
迁移成本不高,但收益显著。如果你的项目还在用客户端 fetch + innerHTML,强烈建议试试这个方案。