文章目录
目录导航
快速定位文章内容
正文
刚开始接触 Next.js 的时候,我最容易卡住的不是语法。
不是
async 组件。
不是路由。
也不是数据请求本身。真正让我反复犹豫的是另一件事:
这一段东西,到底该写在服务端,还是客户端?
因为你会发现,Next.js 不是单纯在教你“怎么写页面”,它其实还在逼你做一层判断:
- 哪些内容更适合在服务端先准备好
- 哪些交互必须放到浏览器里再执行
- 哪些组件看起来只是个 UI,但其实会把整页都拖进客户端边界
这件事如果一开始没想清楚,项目后面通常会出现两种情况:
- 要么什么都往客户端放,最后页面首屏越来越重
- 要么什么都想留在服务端,结果交互一做就开始别扭
所以我后来给自己定了一个很实用的判断方式。
不是先问“这个功能能不能放客户端”。
而是先问:
它到底更像内容,还是更像交互?
如果它主要是“展示内容”,我会优先想服务端
比如这些页面:
- 文章详情页
- 标签归档页
- 工具介绍页
- 专题页
- 首页的内容列表
这类东西的共同点很明显:
- 它们首屏就要给用户看
- 结构相对稳定
- 主要任务是把内容展示出来
- 很多时候还希望 SEO 更好一点
这种场景下,我现在默认会先往服务端想。
比如文章详情页:
export default async function PostPage({ params }: Props) { const post = await getPostBySlug(params.slug) return <PostDetail post={post} /> }
这里服务端组件的好处很直接:
- 数据可以在渲染前准备好
- HTML 一开始就更完整
- 页面内容更容易被搜索引擎拿到
- 不需要为了拿一段正文再在客户端多跑一次请求
对内容站来说,这一点特别重要。
因为用户来你站里,不是先来点按钮的。
大多数时候,他是先来读内容的。
既然如此,那最先到达浏览器的就应该是内容本身。
如果它主要是“即时交互”,我会放客户端
再看另一类东西:
- 筛选面板展开收起
- 搜索关键词输入
- 点赞按钮
- 复制按钮
- 主题切换
- Tabs 切换
- Modal 开关
这类场景的核心不是“把内容提前准备好”,而是“用户一操作,界面立刻响应”。
这种我基本不会犹豫,直接放客户端。
"use client" import { useState } from "react" export function SearchPanel() { const [keyword, setKeyword] = useState("") return ( <input value={keyword} onChange={(e) => setKeyword(e.target.value)} placeholder="搜点什么" /> ) }
因为这类功能本来就依赖:
- 浏览器事件
- 本地状态
- 立刻更新界面
你硬把它往服务端思路里塞,反而会把结构搞得很拧巴。
真正要小心的,是“看起来不复杂,但会把边界带歪”的组件
我后来踩坑最多的,不是大功能。
反而是一些看起来很小的东西。
比如一个文章页顶部信息区,本来只是:
- 标题
- 发布时间
- 标签
- 阅读时长
按理说这完全可以是服务端组件。
但如果我顺手在里面塞了一个:
- 收藏按钮
- 点赞按钮
- 分享弹层
- 复制链接按钮
它就很可能开始需要
use client。一旦你直接在这个大组件顶部写了
"use client",问题就来了:原本只是一个小按钮的交互,最后可能把整块内容区域都拖进客户端。
这就是我现在很在意的一件事:
不要因为一点点交互,把本来适合留在服务端的内容区整块搬走。
我现在更习惯“服务端做骨架,客户端补局部交互”
这是我现在最常用的一种拆法。
比如文章详情页,我会倾向于这样:
- 页面主体、正文、标签、相关推荐都留在服务端
- 点赞、复制链接、目录高亮、评论输入这些小块单独做客户端组件
像这样:
export default async function PostPage({ params }: Props) { const post = await getPostBySlug(params.slug) return ( <article> <PostHeader post={post} /> <PostBody content={post.content} /> <PostActions postId={post.id} /> </article> ) }
然后:
"use client" export function PostActions({ postId }: { postId: number }) { // 点赞、复制、分享这些交互放这里 }
这样拆的好处很现实:
- 内容部分继续享受服务端渲染的优势
- 交互部分独立存在,不会污染整页边界
- 后面维护时,也更容易一眼看出谁负责展示、谁负责交互
我判断要不要放客户端时,通常会先问这几个问题
1. 它需要浏览器事件吗
比如:
onClickonChangeonKeyDown
如果高度依赖这些,那基本就是客户端组件。
2. 它需要本地状态吗
比如:
- 展开收起
- 输入中的值
- 当前选中项
- 本地乐观更新
这类也通常更适合客户端。
3. 它首屏是不是必须出现
如果这个内容用户一打开页面就该看到,而且本身又是主要内容,那我会优先考虑服务端。
4. 它是不是会影响 SEO 和首屏完整度
文章、标题、摘要、正文、归档列表这类,一般都更值得放在服务端先生成出来。
5. 我是不是为了一个按钮,把整块大区域都拖进客户端了
这个问题非常有用。
如果答案是“是”,那我通常会停一下,重新拆组件。
一个很常见的误区:把“会变化”误解成“必须客户端”
很多人刚接触这套模式时,会有一个自然反应:
只要数据会变,那就应该放客户端。
但其实不一定。
比如文章列表会变,标签页内容会变,首页推荐也会变。
可这不代表它们必须在浏览器里现拉。
它们完全可以在服务端拿完数据再输出。
只要这次请求返回的是最新内容,对用户来说照样是“新的”。
所以真正该区分的,不是“会不会变”。
而是:
这个变化,发生在请求前,还是发生在用户当前这次浏览器交互里?
如果是请求前就已经确定的内容,服务端完全能处理。
如果是用户此刻点一下、输一下、切一下才触发的变化,那才更像客户端的事。
最后
我后来越来越觉得,Next.js 里最重要的不是记住多少新概念。
而是慢慢建立一种判断:
- 内容尽量早点到页面
- 交互尽量只包住必要范围
- 不要为了一个小按钮,把整页都拖进客户端
- 也不要为了坚持纯服务端,把交互写得很拧巴
如果让我用一句话总结我现在的习惯,就是:
先把页面当内容来搭,再把真正需要动的那一小部分交给客户端。