文章

Next.js

在 Next.js 里,什么内容该放服务端,什么内容该放客户端

发布于2026-05-09更新于2026-05-09阅读时间7 分钟
文章目录
正文
刚开始接触 Next.js 的时候,我最容易卡住的不是语法。
不是 async 组件。 不是路由。 也不是数据请求本身。
真正让我反复犹豫的是另一件事:
这一段东西,到底该写在服务端,还是客户端?
因为你会发现,Next.js 不是单纯在教你“怎么写页面”,它其实还在逼你做一层判断:
  • 哪些内容更适合在服务端先准备好
  • 哪些交互必须放到浏览器里再执行
  • 哪些组件看起来只是个 UI,但其实会把整页都拖进客户端边界
这件事如果一开始没想清楚,项目后面通常会出现两种情况:
  • 要么什么都往客户端放,最后页面首屏越来越重
  • 要么什么都想留在服务端,结果交互一做就开始别扭
所以我后来给自己定了一个很实用的判断方式。
不是先问“这个功能能不能放客户端”。 而是先问:
它到底更像内容,还是更像交互?

如果它主要是“展示内容”,我会优先想服务端

比如这些页面:
  • 文章详情页
  • 标签归档页
  • 工具介绍页
  • 专题页
  • 首页的内容列表
这类东西的共同点很明显:
  • 它们首屏就要给用户看
  • 结构相对稳定
  • 主要任务是把内容展示出来
  • 很多时候还希望 SEO 更好一点
这种场景下,我现在默认会先往服务端想。
比如文章详情页:
tsx
export default async function PostPage({ params }: Props) { const post = await getPostBySlug(params.slug) return <PostDetail post={post} /> }
这里服务端组件的好处很直接:
  • 数据可以在渲染前准备好
  • HTML 一开始就更完整
  • 页面内容更容易被搜索引擎拿到
  • 不需要为了拿一段正文再在客户端多跑一次请求
对内容站来说,这一点特别重要。
因为用户来你站里,不是先来点按钮的。 大多数时候,他是先来读内容的。
既然如此,那最先到达浏览器的就应该是内容本身。

如果它主要是“即时交互”,我会放客户端

再看另一类东西:
  • 筛选面板展开收起
  • 搜索关键词输入
  • 点赞按钮
  • 复制按钮
  • 主题切换
  • Tabs 切换
  • Modal 开关
这类场景的核心不是“把内容提前准备好”,而是“用户一操作,界面立刻响应”。
这种我基本不会犹豫,直接放客户端。
tsx
"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",问题就来了:
原本只是一个小按钮的交互,最后可能把整块内容区域都拖进客户端。
这就是我现在很在意的一件事:
不要因为一点点交互,把本来适合留在服务端的内容区整块搬走。

我现在更习惯“服务端做骨架,客户端补局部交互”

这是我现在最常用的一种拆法。
比如文章详情页,我会倾向于这样:
  • 页面主体、正文、标签、相关推荐都留在服务端
  • 点赞、复制链接、目录高亮、评论输入这些小块单独做客户端组件
像这样:
tsx
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> ) }
然后:
tsx
"use client" export function PostActions({ postId }: { postId: number }) { // 点赞、复制、分享这些交互放这里 }
这样拆的好处很现实:
  • 内容部分继续享受服务端渲染的优势
  • 交互部分独立存在,不会污染整页边界
  • 后面维护时,也更容易一眼看出谁负责展示、谁负责交互

我判断要不要放客户端时,通常会先问这几个问题

1. 它需要浏览器事件吗

比如:
  • onClick
  • onChange
  • onKeyDown
如果高度依赖这些,那基本就是客户端组件。

2. 它需要本地状态吗

比如:
  • 展开收起
  • 输入中的值
  • 当前选中项
  • 本地乐观更新
这类也通常更适合客户端。

3. 它首屏是不是必须出现

如果这个内容用户一打开页面就该看到,而且本身又是主要内容,那我会优先考虑服务端。

4. 它是不是会影响 SEO 和首屏完整度

文章、标题、摘要、正文、归档列表这类,一般都更值得放在服务端先生成出来。

5. 我是不是为了一个按钮,把整块大区域都拖进客户端了

这个问题非常有用。
如果答案是“是”,那我通常会停一下,重新拆组件。

一个很常见的误区:把“会变化”误解成“必须客户端”

很多人刚接触这套模式时,会有一个自然反应:
只要数据会变,那就应该放客户端。
但其实不一定。
比如文章列表会变,标签页内容会变,首页推荐也会变。 可这不代表它们必须在浏览器里现拉。
它们完全可以在服务端拿完数据再输出。 只要这次请求返回的是最新内容,对用户来说照样是“新的”。
所以真正该区分的,不是“会不会变”。
而是:
这个变化,发生在请求前,还是发生在用户当前这次浏览器交互里?
如果是请求前就已经确定的内容,服务端完全能处理。 如果是用户此刻点一下、输一下、切一下才触发的变化,那才更像客户端的事。

最后

我后来越来越觉得,Next.js 里最重要的不是记住多少新概念。
而是慢慢建立一种判断:
  • 内容尽量早点到页面
  • 交互尽量只包住必要范围
  • 不要为了一个小按钮,把整页都拖进客户端
  • 也不要为了坚持纯服务端,把交互写得很拧巴
如果让我用一句话总结我现在的习惯,就是:
先把页面当内容来搭,再把真正需要动的那一小部分交给客户端。

信息

文章信息

刚接触 Next.js 时,很多人最容易混乱的不是语法,而是“这段东西到底该写在服务端还是客户端”。本文不讲抽象概念,直接从博客页、工具页、筛选栏、点赞按钮这些常见场景出发,聊聊我现在的判断方式。

最后更新于 2026-05-09阅读时长 7 分钟
Next.js