——从渲染到边界
子曰:
技术之学,非在名词之多,
而在边界之明、时序之辨。
一、论渲染之本
子曰:
页面者,HTML 也;
交互者,JavaScript 也。
前端之始,在于两个问题:
HTML 是何时生成的?
JS 是在何处执行的?
不明此二者,
则 CSR、SSR、RSC 之论,
皆成空谈。
二、论客户端渲染(CSR)
子曰:
CSR 者,
HTML 空而 JS 满也。
其形也
<!-- index.html -->
<body>
<div id="root"></div>
<script src="/app.js"></script>
</body>
// app.js
fetch("/api/data")
.then(res => res.json())
.then(data => {
document.getElementById("root").innerHTML =
`<h1>${data.title}</h1>`
})
页面初至,无文无物;
待 JS 下载、执行、取数之后,
方始成形。
其得失
其利:
- 架构简单
- 前后端分离
- 易于起步
其弊:
- 首屏慢
- SEO 弱
- JS 体积膨胀
故曰:
CSR 易学,
难久。
三、论服务器渲染(SSR)
子曰:
SSR 者,
HTML 先成,而后交于浏览器也。



其形也
// server
app.get("/", async (req, res) => {
const data = await getData()
const html = renderToString(<Page data={data} />)
res.send(html)
})
服务器代浏览器:
- 获取数据
- 渲染 HTML
- 返回首屏内容
浏览器初见,即有其形。
其得失
其利:
- 首屏快
- SEO 佳
- 用户感知好
其弊:
- 服务器压力增大
- 若等待所有数据,
则慢于最慢接口
四、论 Hydration
子曰:
HTML 可见,
未必可用。
// client
hydrateRoot(
document.getElementById("root"),
<Page />
)
SSR 返回的 HTML:
- 只能“看”
- 不能“点”
JavaScript 到来后:
- 绑定事件
- 接管状态
- 页面始可交互
此谓 Hydration。
五、论服务器组件(RSC)
子曰:
RSC 者,
非渲染之术,
乃代码归属之道也。

Server Component 之形
// Server Component
export default async function Page() {
const user = await db.user.findFirst()
return <h1>Hello {user.name}</h1>
}
其特点:
- 只运行在服务器
- 可直连数据库
- 不进入浏览器 JS
六、论 RSC 非 SSR
子曰:
世人多以 RSC 为 SSR 之变,
是大误也。
SSR 关心:HTML 何时生成?
RSC 关心:代码在何处执行?
RSC 的核心价值在于:
- 减少 JS 体积
- 明确安全边界
- 数据逻辑回归服务器
七、论客户端组件与服务器组件
子曰:
非曰尽为服务器,
亦非曰尽归客户端。
// Server Component
import LikeButton from "./LikeButton"
export default function Page() {
return (
<>
<Article />
<LikeButton />
</>
)
}
// Client Component
"use client"
export default function LikeButton() {
return <button>Like</button>
}
Server 负责:
- 数据
- 结构
- 边界
Client 负责:
- 交互
- 状态
- 动效
八、论服务器边界
子曰:
浏览器者,不可信也;
服务器者,可托命也。
Client Component
↓
Server Action / API
↓
Database
客户端只发请求,
服务器才做鉴权与访问。
九、论 Token 之存与不存
子曰:
Token 不可见于 JavaScript。
Set-Cookie: auth=xxx; HttpOnly; Secure
fetch("/api/user", {
credentials: "include"
})
客户端不知 Token,
浏览器自携 Cookie。
十、论 Server Actions
子曰:
Action 者,
服务器之事也。
"use server"
export async function createPost(formData: FormData) {
const user = getUserFromCookie()
await db.post.create({ userId: user.id })
}
<form action={createPost}>
<button>Submit</button>
</form>
其形似函数,
其实为一次受控请求。
十一、论 Suspense
子曰:
Suspense 者,
声明等待之边界也。
<Suspense fallback={<Loading />}>
<Feed />
</Suspense>
不阻全局,
先出可出之部分。
十二、论 Streaming
子曰:
Streaming 者,
非一次尽出,
乃边算边发。



HTML 先到:
<header />
<aside />
<div>Loading...</div>
内容后补:
<script>
insertFeedHTML()
</script>
十三、论网络之象
子曰:
Streaming 之请求,
非多次,乃一次;
非骤止,乃绵延。
Network 面板中:
TTFB 很短
Content Download 很长
十六、论 RSC、SSR、Streaming 之合
子曰:
RSC 多 async,
async 必有等待;
有等待而不阻全局,
需 Suspense;
有先后而不待齐,
需 Streaming。
三者合,
则首屏速而系统稳。
十七、总论
CSR:浏览器做一切
SSR:服务器先渲染 HTML
RSC:服务器执行组件代码
Suspense:声明等待边界
Streaming:HTML 分段到达
Server Action:安全触发服务器逻辑
十八、结语
技术之进,
非在框架之新,
而在边界之清。
知代码所处,
明职责所在,
则复杂自消。
善前端者,不乱写组件;
善架构者,不乱放代码。
善哉。