Next.js App Router:先把 Server 与 Client 组件的边界划清楚
Next.js App Router:先把 Server 与 Client 组件的边界划清楚
用 Next.js App Router 写页面时,最容易踩的坑不是路由配错,而是组件边界划错:一个本该在服务端渲染的组件被标成客户端组件,结果把数据库查询、密钥都带进了浏览器包;或者反过来,一个需要交互的组件放在服务端,白白丢了交互能力。边界其实只有一条规则:默认所有组件都是 Server Component,只有真正需要浏览器能力时才加 "use client"。
先分清:哪些能力只能在服务端,哪些必须在客户端
Server Component 可以直接读数据库、调内部 API、读环境变量里的密钥,因为它的代码永远不会发到浏览器。Client Component 则负责交互:点击、输入、动画、useState、useEffect、浏览器 API。一个页面里两者可以混用,但数据流只能单向:Server Component 可以把组件和可序列化的数据传给 Client Component,反过来不行。
// app/posts/page.tsx —— Server Component,默认不需要标记
import PostList from "./post-list";
export default async function PostsPage() {
const posts = await db.post.findMany({ take: 10 }); // 只在服务端执行
return <PostList posts={posts} />;
}
// app/posts/post-list.tsx —— 需要交互,才标记为客户端组件
"use client";
import { useState } from "react";
export default function PostList({ posts }) {
const [query, setQuery] = useState("");
const shown = posts.filter((p) => p.title.includes(query));
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<ul>{shown.map((p) => <li key={p.id}>{p.title}</li>)}</ul>
</>
);
}
三个常见的边界错误
第一,把 "use client" 写在页面顶层。标记一旦加在父组件上,整棵子树都会变成客户端组件,服务端渲染的收益就丢了一大半。正确做法是把标记下移到真正需要交互的叶子组件。
第二,向 Client Component 传函数或不可序列化的对象。Server 到 Client 的 props 必须可序列化:传数据库连接、Date 对象里的方法、函数都会在边界处报错或被悄悄丢掉。需要回调时,改用 Server Action 或把交互逻辑收进客户端组件内部。
第三,在 Client Component 里读密钥。process.env 里没有 NEXT_PUBLIC_ 前缀的变量在客户端是读不到的,这其实是框架在保护你:真需要密钥的操作,留在 Server Component 或 Server Action 里做。
边界划错时,页面会怎么“报复”你
边界划错的代价往往不是立刻报错,而是慢慢显现:包体积悄悄变大,首屏变慢,敏感逻辑进了浏览器包才被发现。更隐蔽的一种是“水合不一致”:服务端渲染出的 HTML 与客户端首次渲染不一致,页面闪一下才恢复,根源常常是某个本该只在客户端算的值被放进了服务端渲染。遇到这类闪烁,先别急着加 suppressHydrationWarning 掩盖,回到边界上找:这个值到底该在哪一侧算?多数情况下,把它收进最小的客户端组件,问题就消失了。
一个实用的检查清单
写完一个页面后,花一分钟自查三件事:页面级数据获取是否留在服务端;"use client" 是否只出现在最小的交互组件上;传过边界的 props 是否都是普通数据。边界划清楚之后,首屏变快、包变小、密钥也不会外泄——App Router 大部分“性能玄学”,到最后都是边界问题。
- 点赞
- 收藏
- 关注作者
评论(0)