Next.js 渲染方式主线
一、先建立总模型
Next.js 的渲染方式不能只背成 SSR / SSG / ISR / CSR 四个缩写。更稳定的理解方式是先问五个问题:
代码在哪里执行:Server Component、Client Component、Route Handler、Server Action、浏览器。
页面什么时候生成:构建时、请求时、首次访问时、后台再生成、浏览器运行时。
结果缓存在哪里:请求内去重、服务端数据缓存、静态 shell、CDN、浏览器 Router Cache、SWR/React Query。
响应怎么发给浏览器:整页阻塞返回,还是通过 Suspense / Streaming 分块返回。
交互怎么接上:HTML 先展示,Client Component 的 JS 下载后 hydration,事件才真正可用。
一句话主线:
Next.js App Router 的现代模型不是“一个页面选择 SSR 或 SSG”,而是在同一棵组件树里切分:哪些内容静态预渲染,哪些内容缓存,哪些内容请求时动态生成,哪些内容流式返回,哪些内容交给浏览器交互。
可以把它画成这样:
Next.js App Router 页面
│
├─ Server Components 默认执行
│ ├─ 可预渲染? -> SSG / Static Shell
│ ├─ 可缓存? -> use cache / cacheLife / cacheTag
│ ├─ 请求态? -> Dynamic Rendering / SSR
│ └─ 慢数据? -> Suspense + Streaming
│
├─ Client Components
│ ├─ 首屏可被服务端预渲染成 HTML
│ ├─ JS 发送到浏览器
│ └─ hydration 后获得点击、输入、状态等交互能力
│
└─ Cache Layers
├─ request memoization
├─ server data / component cache
├─ prerender / static shell cache
├─ CDN / reverse proxy cache
├─ browser router cache
└─ client data cache, such as SWR / React Query一次请求的完整流水线
首次访问
首次打开一个 App Router 页面时,大致流程是:
浏览器请求 URL。
Next.js 根据路由段、组件树、数据访问方式判断哪些内容可预渲染、哪些需要请求时生成。
Server Components 在服务端执行,生成 RSC Payload。
Client Components 的引用、props 和占位信息也会写进 RSC Payload。
Next.js 用 RSC Payload 和 Client Components 预渲染出 HTML。
浏览器先展示 HTML,这时页面可能还不可交互。
浏览器下载 Client Component 的 JS。
React hydration,把事件处理器接到 DOM 上。
如果有 Suspense / Streaming,慢组件完成后继续流式补齐内容。
关键点:
HTML 解决“先看到什么”
RSC Payload 解决“服务端组件树如何在客户端对齐”
Client JS + hydration 解决“如何交互”客户端导航
通过 <Link> 或 router 导航时,Next.js 不一定重新加载完整 HTML:
预取或请求目标路由的 RSC Payload。
浏览器复用已有 layout 和 Client Component 状态。
React 根据新的 RSC Payload 更新页面。
Client Components 通常在客户端继续渲染,不需要重新拿一份完整服务端 HTML。
所以要区分:
首屏加载:HTML + RSC Payload + hydration
客户端导航:主要依赖 RSC Payload + Router Cache二、核心渲染方式:总览与拆解
这张表用于先建立“横向地图”。后面的章节再逐个拆。
渲染方式 | 解决的问题 | 生成位置 | 生成时机 | 是否依赖请求态 | 缓存特征 | 首屏表现 | 数据新鲜度 | 典型触发 / API | 适合场景 | 主要代价 |
|---|---|---|---|---|---|---|---|---|---|---|
SSG / Static Rendering | 提前生成公共页面 | 构建环境 / 服务端预渲染阶段 | build time 或预渲染阶段 | 否 | 最容易 CDN 缓存 | 快,HTML 直接可用 | 低到中,取决于重新生成策略 |
| 文档、营销页、博客详情、公开内容 | 内容更新不够即时,大量页面会拉长构建或预渲染成本 |
SSR / Dynamic Rendering | 每次请求拿到最新或用户相关内容 | 服务端 Node.js / Edge runtime | request time | 是 | 通常不做公共长缓存 | 首屏有 HTML,但 TTFB 依赖服务端和数据源 | 高 |
| 登录态页面、权限页、实时搜索、个性化 Dashboard | 服务端压力更大,CDN 友好度较低 |
ISR | 静态速度 + 可更新内容 | 服务端缓存 + 后台再生成 | 先静态,过期或失效后再生成 | 通常否,或只对公共数据 | 可缓存,按时间或事件失效 | 大多数请求很快 | 中到高,有 stale 窗口 | 旧模型 | CMS、新闻、商品页、博客列表 | 需要接受短暂旧数据,缓存一致性要设计 |
CSR | 浏览器负责主要渲染和交互 | 浏览器 | JS 下载执行之后 | 可依赖浏览器态 | 由浏览器、SWR、React Query 等控制 | 可能先看到壳或 loading | 取决于客户端请求策略 |
| 强交互局部、后台工具、聊天、图表、非 SEO 核心内容 | 首屏和 SEO 弱,bundle 和 hydration 成本更高 |
RSC / Server Components | 让组件在服务端执行,减少客户端 JS | 服务端 | 可发生在 SSG、SSR、ISR 中 | 取决于组件是否读取请求态 | 可被预渲染或缓存,也可动态 | 可输出 HTML + RSC Payload | 取决于数据策略 | App Router 默认;不用 | 数据获取、权限校验、密钥访问、重 UI 轻交互区域 | 需要理解 Server / Client 边界和可序列化 props |
Streaming | 慢数据不阻塞整页 | 服务端响应流 + 浏览器渐进接收 | request time 分块发送 | 常用于动态或慢数据 | 本身不是缓存策略 | 静态 shell / fallback 先出现 | 取决于内部数据 |
| 搜索、推荐、AI 输出、报表、个性化小区块 | loading 边界、错误边界和布局稳定性要设计 |
PPR / Partial Prerendering | 静态 shell 与动态区块共存 | 构建 / 预渲染 + 请求时恢复 | 静态部分先预渲染,动态部分请求时流式补齐 | 动态区块可以依赖请求态 | shell 可缓存,动态区块单独处理 | 很快看到页面骨架和公共内容 | 分区块决定 | Cache Components 下的默认方向, | 大多数“公共壳 + 用户态洞”的页面 | 对部署平台、流式响应、缓存协调要求更高 |
再压缩成选择口诀:
公共且稳定 -> SSG
公共但会更新 -> ISR
依赖请求态或必须实时 -> SSR / Dynamic Rendering
强交互或浏览器态明显 -> CSR / Client Component
慢数据不该阻塞整页 -> Streaming
App Router 默认组件模型 -> RSC
静态壳 + 动态洞 -> PPRSSG / SSR / ISR / CSR 的真实区别
总体对比
维度 | SSG | SSR / Dynamic Rendering | ISR | CSR |
|---|---|---|---|---|
完整名称 | Static Site Generation | Server-Side Rendering / Dynamic Rendering | Incremental Static Regeneration | Client-Side Rendering |
核心问题 | 能不能提前生成 | 是否每次请求生成 | 静态内容如何更新 | 是否主要交给浏览器生成 |
HTML 生成时机 | 构建时 / 预渲染时 | 每次请求时 | 首次静态生成,过期或失效后再生成 | 浏览器执行 JS 后生成主要内容 |
数据获取位置 | 构建 / 预渲染阶段 | 服务端请求阶段 | 构建 / 后台再生成阶段 | 浏览器请求 API |
是否有完整首屏 HTML | 有 | 有 | 有 | 通常没有完整业务内容,除非外层由服务端预渲染 |
SEO | 好 | 好 | 好 | 相对弱 |
TTFB | 低 | 取决于服务器和数据源 | 大多数时候低 | HTML TTFB 低,但内容出现晚 |
数据新鲜度 | 低,除非重建 | 高 | 中高,可配置 | 高,取决于客户端请求 |
服务器运行时成本 | 低 | 高 | 中低 | 页面渲染低,API 调用另算 |
CDN 友好度 | 高 | 低到中 | 高 | 静态壳高,数据接口另算 |
个性化能力 | 弱 | 强 | 弱到中,不适合强登录态 | 强 |
用户可能看到旧数据 | 是 | 通常否 | 是,有 stale 窗口 | 取决于客户端缓存 |
典型场景 | 文档、营销页、博客 | 登录态、权限页、搜索 | CMS、商品、新闻 | 编辑器、聊天、图表、复杂后台交互 |
常见误区 | 把所有公共内容都 SSR | 为了“新鲜”牺牲所有缓存 | 忘记 stale 窗口和失效策略 | 把整站做成空壳 SPA |
SSG:提前生成公共内容
SSG 的核心是:
构建或预渲染阶段生成 HTML / RSC Payload
线上请求直接复用静态结果适合:
营销页。
文档页。
不频繁变化的博客详情页。
公共的标签页、分类页。
优点:
响应快。
CDN 友好。
服务端运行时压力小。
SEO 稳定。
代价:
内容变化需要重新生成。
大量页面会增加构建或预渲染压力。
不适合强用户态、权限态、地域态内容。
App Router 里的动态路由常配合:
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map((post) => ({ slug: post.slug }))
}generateStaticParams 用来告诉 Next.js 哪些动态参数可以提前生成。
SSR / Dynamic Rendering:每次请求生成
SSR 的核心是:
请求来了
服务端读取请求态或实时数据
生成 HTML / RSC Payload
返回给浏览器在 App Router 里,更准确的叫法常常是 Dynamic Rendering。触发动态渲染的常见信号包括:
cookies():读取用户 cookie。headers():读取请求头。searchParams:依赖 URL 查询参数。draftMode():草稿预览。connection():显式等待请求,退出预渲染。fetch(..., { cache: 'no-store' }):每次请求重新获取。权限、登录态、地域、AB 实验等只能请求时确定的数据。
适合:
登录后的 Dashboard。
权限页、草稿预览页。
搜索结果页。
每次请求都必须最新的页面。
代价:
TTFB 更依赖服务端和数据库。
服务端压力更大。
公共 CDN 缓存更难设计。
面试表达:
SSR 解决的是“请求时拿到最新或用户相关首屏 HTML”的问题,但它不是性能银弹。它把渲染压力从浏览器转移到服务端,所以要配合缓存、Streaming、静态 shell 和边界拆分。
ISR:静态速度 + 后台更新
ISR 的核心是:
大多数请求命中静态缓存
缓存过期或主动失效后触发重新生成
生成成功后替换缓存典型流程:
构建或首次请求时生成页面。
用户请求时返回缓存结果。
超过 revalidate 时间或收到失效指令。
下一次请求触发再生成。
再生成成功后,后续请求拿到新结果。
如果再生成失败,通常继续服务上一次成功的结果。
ISR 和 SSR 的关键区别:
SSR:每次请求都重新生成
ISR:多数请求复用缓存,只在过期或失效后再生成ISR 适合:
博客详情。
CMS 页面。
商品详情。
新闻、活动、帮助中心。
不适合:
强用户态页面。
金融价格、库存扣减这种必须绝对实时的核心数据。
修改后所有用户必须立即强一致看到新内容的场景。
旧模型常见写法:
export const revalidate = 60
export default async function Page() {
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 },
}).then((res) => res.json())
return <PostList posts={posts} />
}Cache Components 新模型更偏向:
import { cacheLife, cacheTag } from 'next/cache'
async function getPosts() {
'use cache'
cacheLife('hours')
cacheTag('posts')
return db.post.findMany()
}CSR:浏览器运行时渲染
CSR 的核心是:
浏览器先拿到 HTML 壳和 JS
JS 执行后渲染 UI
组件再请求 API 并更新页面典型写法:
'use client'
import useSWR from 'swr'
export function NotificationPanel() {
const { data, isLoading } = useSWR('/api/notifications', fetcher)
if (isLoading) return <p>Loading...</p>
return <NotificationList data={data} />
}CSR 适合:
强交互组件。
浏览器状态明显的区域,例如拖拽、富文本编辑器、图表。
实时聊天、WebSocket、轮询。
SEO 不核心的后台局部模块。
CSR 不适合:
SEO 核心页面主体。
本可以在服务端安全拿到的数据。
首屏性能敏感的公共内容页。
关键纠偏:
Client Component 不等于 CSR 整页。Client Component 在首次加载时也可以被服务端预渲染成 HTML,只是它的交互逻辑需要发到浏览器并 hydration。
三、RSC / Streaming / PPR:组件边界与传输边界
RSC 解决的问题是:
这个组件在哪里执行?
哪些代码需要进入客户端 bundle?
哪些数据可以直接在服务端读取?SSR / SSG / ISR 解决的问题是:
HTML 和 RSC Payload 什么时候生成?
生成结果如何复用?所以它们不是同一维度:
维度 | 关注点 | 示例 |
|---|---|---|
RSC | 组件在哪里执行 | Server Component 直接查数据库 |
SSR | 请求时生成 | 每个请求读取 cookie 后生成页面 |
SSG | 提前生成 | 构建时生成博客页 |
ISR | 缓存后再生成 | 商品页每 10 分钟更新 |
CSR | 浏览器生成 / 交互 | 编辑器、图表、聊天输入框 |
Server Component 适合做什么
Server Component 适合:
数据获取。
访问数据库、文件系统、内部服务。
使用密钥、token、私有环境变量。
渲染不需要浏览器交互的大块 UI。
减少客户端 JS bundle。
示例:
export default async function PostPage({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await db.post.findUnique({ where: { slug } })
return <Article post={post} />
}Client Component 适合做什么
Client Component 适合:
useState、useReducer。onClick、onChange等事件。useEffect。window、document、localStorage、浏览器定位等 API。第三方只支持客户端的组件库。
示例:
'use client'
import { useState } from 'react'
export function LikeButton({ initialLiked }: { initialLiked: boolean }) {
const [liked, setLiked] = useState(initialLiked)
return (
<button => setLiked((value) => !value)}>
{liked ? 'Liked' : 'Like'}
</button>
)
}use client 边界
'use client' 不是给组件“开启客户端能力”的局部开关,而是一个 模块图边界:
某个文件标记 'use client'
-> 这个文件及其 imports 会进入客户端 bundle
-> 子组件也会在客户端边界内使用因此原则是:
尽量把
'use client'放到叶子交互组件。不要把整个
layout.tsx或大页面都标成 Client Component。服务端数据获取和密钥访问留在 Server Component。
Server Component 可以把可序列化 props 传给 Client Component。
Streaming:把整页等待拆成多个等待
传统 SSR 的阻塞模型:
所有数据都准备好
服务端生成完整 HTML
一次性返回Streaming 的模型:
静态 shell / fallback 先返回
慢组件继续在服务端执行
完成后作为 chunk 流式补齐核心边界是 Suspense:
import { Suspense } from 'react'
export default function Page() {
return (
<>
<Header />
<Suspense fallback={<PriceSkeleton />}>
<LivePrice />
</Suspense>
</>
)
}路由级 loading:
app/dashboard/loading.tsxloading.tsx 相当于 Next.js 自动给 route segment 加了一个 Suspense fallback。
Streaming 的价值:
不让慢数据阻塞整页。
让用户更早看到导航、结构、骨架屏。
适合慢查询、推荐、AI 输出、报表、搜索。
和 SSR、RSC、ISR、CSR 都不是互斥关系。
面试表达:
Streaming 改善的是感知性能,不是简单减少总渲染时间。它通过 Suspense 边界让可用内容先到达浏览器,慢内容后续补齐。
PPR:静态壳 + 动态洞
PPR,Partial Prerendering,核心思想是:
一个页面的大部分内容可以预渲染成静态 shell
少数动态内容在请求时流式补齐示例页面:
商品详情页
│
├─ Header:静态
├─ 商品标题 / 描述 / 图片:ISR 或 use cache
├─ 实时价格:请求时动态 + Suspense
├─ 用户购物车状态:请求时动态 + Suspense
└─ 加入购物车按钮:Client Component这比“整页 SSR”更精细:
不要因为一个 UserMenu 读 cookie,就让整个页面都失去静态收益。边界设计原则:
静态 shell 往外层放。
请求态内容往深层放。
慢内容用 Suspense 包住。
可复用公共数据用缓存包住。
真正交互的局部再切成 Client Component。
四、缓存边界与动态边界
Next.js 渲染的难点不只是“什么时候渲染”,而是“复用了什么”。
缓存层级总表
缓存层 | 缓存什么 | 发生在哪里 | 生命周期 | 解决的问题 | 常见误区 |
|---|---|---|---|---|---|
Request Memoization | 同一次 render pass 内相同 GET | 单次服务端渲染过程 | 本次请求 / 本次渲染 | 避免组件树里重复请求同一数据 | 以为它是跨请求缓存 |
Data Cache / Cache Components | 数据函数、异步计算结果 | Next.js 服务端缓存 |
| 复用昂贵数据查询 | 把用户私有数据误放进公共缓存 |
UI / Component Cache | 组件或页面的渲染结果 | Next.js 服务端缓存 |
| 复用稳定 UI 片段 | 缓存里直接读 cookie/header |
Prerender / Static Shell Cache | 预渲染 HTML 和 RSC Payload | Next.js / 部署平台 | build、revalidate、失效后更新 | 快速返回公共壳 | 以为一个动态小块必须拖垮整页 |
CDN / Reverse Proxy Cache | HTTP 响应、静态资源、图片、脚本 | CDN / 反向代理 | Cache-Control / 平台策略 | 降低源站压力和延迟 | Next 内部 tag 失效不一定自动清外部 CDN |
Router Cache | 客户端导航用的 RSC Payload | 浏览器内存 | 会话内、预取、刷新后变化 | 加速客户端导航 | 以为它等于 HTTP 缓存 |
Client Data Cache | SWR / React Query 数据 | 浏览器内存 / 持久化存储 | 由库配置 | 客户端请求去重、重试、刷新 | 和 Next 服务端缓存混为一谈 |
Cache Components 新模型
Next.js 16 的新方向是 Cache Components。它不是所有旧项目的默认前提,需要显式开启:
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig数据级缓存:
import { cacheLife, cacheTag } from 'next/cache'
export async function getProducts() {
'use cache'
cacheLife('hours')
cacheTag('products')
return db.product.findMany()
}UI 级缓存:
import { cacheLife } from 'next/cache'
export default async function ProductList() {
'use cache'
cacheLife('minutes')
const products = await db.product.findMany()
return <ul>{products.map((item) => <li key={item.id}>{item.name}</li>)}</ul>
}核心规则:
'use cache'可以用于 async function、component、page、layout。参数和闭包值会参与 cache key。
cacheLife()控制生命周期。cacheTag()给缓存打标签,便于主动失效。未缓存但需要请求时数据的组件,应放进
<Suspense>。读取
cookies()、headers()、searchParams等 runtime API 的内容,不应该直接放进缓存 scope。启用 Cache Components 后,
dynamic、revalidate、fetchCache等部分旧 route segment 配置不再适用。Cache Components 当前要求 Node.js runtime;如果某个 route 必须跑在 Edge,需要单独评估。
Revalidation:缓存如何变新
API | 作用 | 常用位置 | 适合场景 |
|---|---|---|---|
| 时间型 revalidate |
| 内容按分钟、小时、天刷新 |
| 给缓存项打标签 |
| 后续按业务对象失效 |
| 将某个 tag 标记为 stale | Server Action / Route Handler | CMS 发布后让相关内容后台更新 |
| 立即过期某个 tag | Server Action | 用户提交后立刻读到最新数据 |
| 让某个 path / layout / route handler 失效 | Server Action / Route Handler | 文章发布后刷新 |
容易混淆的点:
revalidatePath('/blog')
-> 主要影响这个路径的页面/布局缓存
revalidateTag('posts')
-> 影响所有使用 posts tag 的缓存数据
updateTag('posts')
-> 更偏“写后读一致性”,让下一次读取必须拿新数据旧模型仍然要能看懂
很多项目还没有启用 Cache Components,所以仍然会看到旧模型:
fetch(url, { cache: 'force-cache' })
fetch(url, { cache: 'no-store' })
fetch(url, { next: { revalidate: 60, tags: ['posts'] } })
export const revalidate = 60
export const dynamic = 'force-dynamic'
export const fetchCache = 'force-no-store'
import { unstable_cache } from 'next/cache'新旧模型对照:
旧模型 | 新模型方向 | 说明 |
|---|---|---|
|
| 从 route segment 配置转向缓存 scope |
| 缓存数据函数 + | 把缓存策略靠近数据函数 |
|
| tag 仍然是重要失效维度 |
|
| 新模型 API 更统一 |
| uncached runtime data / | 新模型下页面默认可动态,缓存是显式选择 |
|
| 缓存边界从 fetch 配置上移到函数/组件 |
面试中可以这样说:
如果是老项目,我会读
fetch选项、route segment config 和unstable_cache;如果是新项目,我会优先考虑 Cache Components,用use cache、cacheLife、cacheTag把缓存边界放在数据函数或组件附近。
动态边界如何切
不理想写法
import { cookies } from 'next/headers'
export default async function Layout({ children }: { children: React.ReactNode }) {
const cookieStore = await cookies()
const theme = cookieStore.get('theme')?.value
return (
<html>
<body data-theme={theme}>{children}</body>
</html>
)
}问题:
过早读取请求态数据。
可能让很大的布局树进入动态渲染。
静态 shell 和缓存收益变少。
更好的切法
import { cookies } from 'next/headers'
import { Suspense } from 'react'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<Header />
<Suspense fallback={<UserMenuSkeleton />}>
<UserMenu />
</Suspense>
{children}
</body>
</html>
)
}
async function UserMenu() {
const cookieStore = await cookies()
const session = cookieStore.get('session')?.value
const user = await getUser(session)
return <UserAvatar user={user} />
}原则:
静态内容往上放
请求态内容往下放
慢内容用 Suspense 包
可复用数据用 cache 包
交互逻辑放小的 Client Component五、路由口径与项目落点
面试里常会混问旧 API 和新模型,所以要能对应:
Pages Router | App Router / 新模型 | 含义 |
|---|---|---|
| Server Component + cached fetch / | 静态或缓存数据获取 |
|
| 提前生成动态路由参数 |
|
| 未生成路径返回 404 |
|
| 未生成路径请求时生成 |
| Dynamic Rendering / runtime APIs / uncached data | 请求时服务端渲染 |
| ISR / | 静态结果定期再生成 |
API Routes | Route Handlers | 服务端 HTTP 接口 |
需要注意:
App Router 不推荐继续用 Pages Router 的 API 口径解释所有问题。
App Router 的重点是组件树、RSC Payload、缓存边界和 Streaming。
同一个页面里可以混合静态、缓存、动态和客户端交互。
项目中的落点
结合 CodeLess Blog AI Native CMS,可以这样选型:
页面 / 模块 | 推荐组合 | 原因 |
|---|---|---|
公开博客详情页 | SSG / ISR + RSC | SEO 重要,内容公共,发布后可 revalidate |
公开博客列表页 | ISR + Streaming | 列表可缓存,推荐或统计区可慢速流式返回 |
标签页 / 分类页 | SSG / ISR | 公共内容,适合 CDN 和 SEO |
文章草稿预览 | Dynamic Rendering | 依赖登录态、权限、草稿状态,不能公共缓存 |
后台文章编辑器 | RSC 初始数据 + Client Component 编辑器 | 初始文章可服务端取,编辑交互必须客户端 |
AI Editor Copilot | Client Component + Server Action / Route Handler streaming | 选区、接受、拒绝是强交互,模型调用必须在服务端 |
AI Chat | Client Component + Streaming API | token 增量展示、停止、重试都依赖客户端交互 |
Trace / Feedback 后台 | Dynamic Rendering + CSR 局部表格 | 登录态、筛选、日志刷新,不适合公共静态化 |
营销介绍页 | SSG / Static Rendering | 内容稳定,首屏和 SEO 优先 |
可以这样表达项目取舍:
在 CodeLess Blog 里,我不会把所有页面都 SSR。公开内容页优先 SSG/ISR,保证 SEO、CDN 命中和低服务器成本;后台、草稿预览、AI Chat、AI Editor 依赖登录态、权限和实时流式交互,会使用 Dynamic Rendering、Client Component 和 Streaming。文章发布后通过 path/tag revalidation 更新公开页,这样兼顾内容新鲜度、首屏速度和服务端成本。
六、面试复习:问答、误区与资料
Q1: Next.js 有哪些渲染方式?如何选择?
我会从生成时机、运行位置、缓存策略和交互需求来选。公共稳定内容用 SSG,公共但需要更新的内容用 ISR,依赖 cookie/header/权限/实时数据的页面用 SSR 或 Dynamic Rendering,强交互局部用 Client Component / CSR。App Router 默认是 RSC,页面内部还可以用 Suspense Streaming 把慢数据拆出去,所以实际项目里通常是混合策略,而不是整页只选一种。
Q2: SSR 和 SSG 的核心区别是什么?
SSR 是请求时生成 HTML,适合实时和用户相关数据,但服务端成本和 TTFB 压力更大。SSG 是构建或预渲染时生成 HTML,响应快、CDN 友好,但内容更新不如 SSR 灵活。如果公共内容需要定期更新,可以在 SSG 基础上用 ISR。
Q3: ISR 解决了什么问题?
ISR 解决的是 SSG 内容更新不灵活的问题。它让页面大多数时候走静态缓存,过期或主动失效后再后台生成。它适合 CMS、博客、商品详情这类公共内容页面,但需要接受短暂 stale 窗口。
Q4: RSC 和 SSR 是一回事吗?
不是。SSR 说的是 HTML 是否在每次请求时生成,RSC 说的是组件代码在哪里执行、哪些代码进入客户端 bundle。一个 Server Component 可以被静态预渲染,也可以在请求时动态渲染。Client Component 负责交互,但也不等于整页 CSR。
Q5: 为什么 use client 不应该乱加?
因为 use client 是模块图边界。标记后该文件及其依赖会进入客户端 bundle,需要下载 JS 并 hydration。只有 state、事件、effect、浏览器 API、第三方客户端组件才应该进入 Client Component;数据获取、密钥访问、可缓存 UI 应尽量留在 Server Component。
Q6: Streaming 解决什么问题?
Streaming 解决慢数据阻塞整页的问题。通过 Suspense,Next.js 可以先返回静态 shell 和 fallback,等慢组件完成后再把内容流式补齐。它提升的是感知性能,尤其适合搜索、推荐、AI 输出、报表等慢区域。
Q7: 如何设计缓存边界?
我会先区分公共数据和用户态数据。公共且可复用的数据放进缓存,用 cacheLife 控制时间,用 cacheTag 做业务失效;用户态、权限态、实时数据不进公共缓存,放进动态组件并用 Suspense 包住。发布或修改内容后,用 revalidatePath 刷具体页面,用 revalidateTag 或 updateTag 刷共享数据。


评论
还没有评论
来发表第一条评论吧!