先记住这个答案
在服务端组件中,您可以定义异步组件并直接 await 数据库查询或 fetch,因为代码运行在服务器上,凭证不会发送到浏览器。客户端组件无法访问数据库,通常通过调用自己的 API 路由或使用 SWR/React Query 获取数据。核心区别是:服务端读取发生在请求时(或构建时),可与缓存配合实现 SSG/SSR 数据预加载;客户端读取发生在浏览器中,交互性更强但凭证有风险且无法供搜索引擎预取。因此,可公开且用于首屏的数据应优先在服务端获取。
- 服务端组件可异步访问数据库或外部 API
- 客户端必须通过 API 或数据请求库获取数据
- 服务端读取是 SSR/SSG 数据预加载的前提
服务端读取为何发生在渲染阶段
在 App Router 中,服务端组件是异步函数。将页面或组件标记为 async 后,您可以直接 await fetch() 或使用 ORM 访问数据库。由于渲染工作在服务器上执行,数据库密码、API 密钥等凭证不会打包进客户端 JS。每个请求都会重新执行渲染(除非启用静态生成),因此数据获取与渲染合为一体,发生在构建时或每次请求期间。
与客户端组件不同,服务端组件不使用 useState、useEffect 等 hook。这些 hook 依赖于浏览器交互和生命周期。如果企图在服务端组件中使用它们,React 会报错,导致页面无法预渲染。因此,凡是需要 SSR/SSG 的首屏数据,必须在服务端获取;交互后的再请求则由客户端另行发起。
展示产品列表的工程取舍
假设要搭建 /products 页面,需从 PostgreSQL 读取产品表,并联合第三方价格 API。产品数据公开且变化不频繁,希望搜索引擎能索引全部内容。选择服务端组件:页面 async function Page() 内同时启动数据库查询和外部 fetch,使用 Promise.all 并行等待;数据库连接直接使用服务端凭证,外部 API 密钥放在 NEXT_PUBLIC_ 以外的环境变量中。
如果改用客户端组件,则必须先创建 API 路由 /api/products 来包装这两个数据源,浏览器加载性能、SEO 和响应速度都会受影响。服务端方案在首屏时,HTML 已含完整数据,并且可以利用 Next.js 的缓存和 revalidate 机制,实现按需增量更新。因此对公开数据、需要预渲染的产品页,服务端读取是更直接、安全的做法。
何时必须放弃纯服务端读取
服务端读取适合用于不依赖浏览器状态的页面:如博客、公开目录。若页面必须读取登录用户的私有数据,且要展示实时头像或个性化设置,则不能把用户 ID 写在客户端请求里,必须让服务端通过身份统一从 session 取得。这种情况仍用服务端读取,但数据结果不能静态缓存,需设为按请求刷新。
另一种边界是数据更新极快且需 WebSocket 推送,例如聊天室或股票行情。服务端每次请求只能拿到瞬间状态,配合 revalidate: 0 会频繁重渲染,风险高。正确做法是外层用服务端提供骨架和首屏,实时部分用独立的客户端组件订阅推送。判断依据:这些数据是否必须在 HTML 里提前出现;若不需要就不该占用服务端渲染,否则会阻塞整条路由。
容易答错的地方
- 误认为服务端组件可以用 `useEffect` 发起请求
- 服务端组件没有生命周期,不能使用
useEffect、useState。它们是异步函数,数据只在渲染开始时获取。若在服务端组件中引入这些 hook,构建时就会类型错误或运行时报错。正确的服务端读取就是直接await数据,若需交互则改用客户端组件。 - 误认为 `fetch` 默认缓存所以服务端数据都能被 CDN 缓存
- 自 Next.js 15 起,
fetch在服务端组件中默认不缓存,每次请求都会重新执行。若需要缓存,必须显式使用next: { revalidate }或设置use cache指令。只有静态生成(SSG)时数据才会在构建时快照。因此,希望通过服务端读取提升性能,必须主动控制缓存策略。
面试官还会怎么问?
服务端读取一定会阻塞页面渲染吗?
如果没有用流式渲染,服务端组件中的 await 会阻塞该路由的渲染,直到所有数据返回。要避免阻塞,可将不重要的数据放到 <Suspense> 边界内,让其他部分先发送给浏览器。官方建议对每个慢数据请求单独用 <Suspense>,配合 loading.js 提供骨架屏。
客户端组件能访问服务端环境变量吗?
不能。服务端环境变量(如 DB 密码)只存在于服务端进程。客户端组件若通过 fetch 调用自己的 API 路由,则由该路由的服务端代码读取环境变量,最终只把响应数据发给浏览器。使用仅客户端变量如 NEXT_PUBLIC_* 才会被公开打包,切勿放入密钥。
若同一份数据在多个服务端组件中用到,会重复请求吗?
每次请求的同一个渲染期内,Next.js 会自动对相同 URL 的 fetch 做 memoization,但仅限一次请求内。如果涉及数据库,推荐用 React 的 cache() 包裹查询函数,确保同一请求的多次调用共享同一 Promise,从而避免重复查询。该缓存不跨请求共享,每次请求都有独立范围。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。