
在 Next.js 中,Server Components (RSC) 的优化潜力常常被低估。常见的性能问题往往源于错误的边界划分。以下是六个常见的错误及改进建议: 1. **在页面顶层直接 `await` 阻塞首屏渲染**:避免在服务端渲染时直接 `await` 获取所有数据,应该将慢操作拆离页面顶层,让页面骨架先显示,再由慢内容补入。 2. **服务端全量数据传递给客户端**:服务端只应传递客户端需要的数据,避免传递大型数据结构,减少序列化、传输和解析的开销。 3. **过度使用“use client”导致用户界面过度饱和**:将交互逻辑拆小,避免将整个大组件下沉到客户端,减少不必要的hydrate过程。 4. **把所有数据都当成首屏必需品**:不是所有数据都是首屏必需的,应该根据页面需求合理规划数据加载顺序,先显示关键内容,再逐步加载其他数据。 5. **忽略组件生命周期优化**:合理使用 `useEffect` 和 `useMemo` 等钩子,避免不必要的渲染和计算,提升组件性能。 6. **忽略浏览器缓存和预加载机制**:利用浏览器缓存和预加载机制,提升页面加载速度和用户体验。 通过正确划分服务端和客户端的职责,可以显著提升 Next.js 应用的性能和用户体验。
在 Next.js 里很容易给人一种“天然更快”的印象:数据放服务端拿,客户端少发点 JavaScript,页面就该更轻、更顺,并且还天然支持SEO。
但真到项目里,事情往往没这么简单。
不少页面接了 RSC 之后,首屏并没有明显变快,交互甚至还更迟钝。问题通常不在 React,也不完全在接口,而在于:你把该放哪儿的逻辑,放错了位置。
RSC 的核心不是“自动提速”,而是重新划分服务端和客户端的职责。边界划对了,体验会很好;边界一旦划错,性能问题就会以另一种方式冒出来。
这类问题,我总结出了最常见的 6 个。
await,把整个首屏一起卡住很多人第一次写 Server Component,都会下意识这么写:
export default async function Page() {
const report = await getReport();
return (
<main>
<Header />
<ReportView data={report} />
</main>
);
}逻辑很顺,但问题也很明显:只要 getReport() 还没返回,React 就没法把任何内容提前流给浏览器。
也就是说,哪怕 Header 本来可以先出来,它也得一起等。
更好的方式,是把慢的部分拆出去,让页面骨架先显示,再让慢内容补进来:
import { Suspense } from "react";
export default function Page() {
return (
<main>
<Header />
<Suspense fallback={<ReportSkeleton />}>
<ReportSection />
</Suspense>
</main>
);
}
async function ReportSection() {
const report = await getReport();
return <ReportView data={report} />;
}
这里优化的重点不是“少等”,而是别让慢数据把整个首屏拖住。
这是第二个特别常见的问题。
很多数据在服务端拿起来很方便,于是顺手就把整包对象传给 Client Component 了。但只要跨过 server/client 边界,这些数据就不是“白送”的——它们要被序列化、传输、解析,最后还要参与 hydration。
比如这样:
export default async function Page() {
const users = await getUsers(); // 包含头像、权限、日志、设置等大量字段
return <UserTable users={users} />;
}如果前端表格实际上只展示姓名、邮箱和状态,那这就是典型的“传多了”。
更合理的写法应该是先裁剪:
export default async function Page() {
const users = await getUsers();
const tableData = users.map((user) => ({
id: user.id,
name: user.name,
email: user.email,
status: user.status,
}));
return <UserTable users={tableData} />;
}原则很简单:客户端需要什么,就只给什么。避免在服务器和客户端之间传递大型数据结构
"use client" 最容易被低估。
它不是“给组件加个交互”的小标记,而是一条很明确的边界:一旦某个组件变成 Client Component,它下面依赖的很多内容也会一起进入客户端环境。
最常见的误用,是为了一个按钮、一个输入框、一个切换器,直接把整个大组件都改成客户端组件:
"use client";
export default function ProductPage({ products }) {
return (
<section>
<SearchBox />
{products.map((item) => (
<ProductCard key={item.id} product={item} />
))}
</section>
);
}这会让本来完全可以留在服务端渲染的大块静态内容,也跟着一起 hydrate。
更稳妥的做法,是把交互拆小,只把真正需要事件处理的部分单独下沉到客户端:
export default function ProductPage({ products }) {
return (
<section>
<SearchBoxClient />
{products.map((item) => (
<ProductCard key={item.id} product={item} />
))}
</section>
);
}"use client";
export function SearchBoxClient() {
return <input placeholder="搜索商品" />;
}一句话说就是:客户端边界能小就不要大。
有些页面并不是“必须等所有东西都回来才能显示”。
但不少项目一上来就把所有请求都堆在页面入口,导致本来可以先显示的标题、导航、列表壳子,也被一起阻塞了。
错误思路一般像这样:
export default async function DashboardPage() {
const stats = await getStats();
const team = await getTeam();
const notices = await getNotices();
return <Dashboard stats={stats} team={team} notices={notices} />;
}如果其中只有 stats 是首屏关键数据,另外两块其实都可以稍后补,那么把它们全绑在一起等,就是在主动放弃 streaming 的优势。
更好的思路是分层:
import { Suspense } from "react";
export default async function DashboardPage() {
const stats = await getStats();
return (
<>
<DashboardHero stats={stats} />
<Suspense fallback={<TeamSkeleton />}>
<TeamSection />
</Suspense>
<Suspense fallback={<NoticeSkeleton />}>
<NoticeSection />
</Suspense>
</>
);
}不是所有数据都值得阻塞首屏。
先让用户看到进展,比一次性全出来更重要。
这个坑不一定体现在“加载很慢”,但会直接影响使用体验。
例如用户刚完成一个操作:新增、删除、修改状态,服务端已经成功了,但页面内容还是旧的。于是用户会怀疑是不是没点上,或者再点一次。
常见场景:
"use server";
export async function updatePost(id: string) {
await db.post.update({
where: { id },
data: { published: true },
});
}如果更新后没有触发重新获取,RSC 渲染出来的结果可能还是旧的。
在 Next.js 里,通常要显式告诉框架这部分内容已经变了:
"use server";
import { revalidatePath } from "next/cache";
export async function updatePost(id: string) {
await db.post.update({
where: { id },
data: { published: true },
});
revalidatePath("/posts");
}也就是说,服务端更新成功,不等于界面会自动变新。
Layout 很适合承载全局结构,但不适合承载太多“每次都得先等”的逻辑。
很多项目喜欢在根布局里一次性拿完用户信息、权限、导航、通知数量、团队配置……看起来很整齐,实际上会把页面切换也一起拖慢。
像这样:
export default async function RootLayout({ children }) {
const user = await getCurrentUser();
const nav = await getNavigation(user.id);
return (
<html>
<body>
<Sidebar nav={nav} />
{children}
</body>
</html>
);
}问题在于,layout 的等待会影响很大一片区域。它本来应该是稳定的外壳,结果却变成了整个应用的总闸门。
更好的做法通常是:
RSC 最大的误区,不是“不会用”,而是“以为接入了就会自动变快”。
它确实提供了更好的渲染模型,但这套模型本身并不会替你决定:
所以,Next.js 里 React Server Components 的性能问题,说到底不是某一个 API 的问题,而是边界管理的问题。
边界放对了,RSC 会很自然地发挥价值。
边界放错了,它也会很自然地把慢和卡藏进你的页面里。