React Server Components 实战:从客户端渲染到 SSG 的完整迁移

#React#Next.js#RSC#SSG

背景

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>
          \`;
        })
    `
  }}
/>

问题

迁移后的代码

新写法: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>
  );
}

优势

性能对比

指标迁移前迁移后提升
首屏内容 (FCP)~800ms~200ms75% ↓
可交互时间 (TTI)~1.2s~200ms83% ↓
JavaScript 体积~45KB~0KB100% ↓
网络请求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 是最优解:

迁移成本不高,但收益显著。如果你的项目还在用客户端 fetch + innerHTML,强烈建议试试这个方案。


完整代码:https://github.com/your-repo/lilnong.top