Next.js SSR/SSG:路由与渲染模式深度解析
Next.js SSR/SSG:路由与渲染模式深度解析> **版本说明**:本文基于 Next.js 14.x 和 15.x 最新版本编写,源码路径参考 `packages/next/src/` 核心模块---## 📑 目录1. [引言:渲染模式的演进](#1-引言渲染模式的演进)2. [Next.js 路由系统架构](#2-nextjs-路由系统架构)3. [SSR(服务器端渲染)深度解析](
Next.js SSR/SSG:路由与渲染模式深度解析
版本说明:本文基于 Next.js 14.x 和 15.x 最新版本编写,源码路径参考
packages/next/src/核心模块
📑 目录
1. 引言:渲染模式的演进
1.1 Web 渲染简史
从传统的服务端渲染(PHP、JSP)到客户端渲染(SPA),再到现代的混合渲染模式,Web 开发经历了一个螺旋式上升的过程。
1.2 Next.js 的核心价值
Next.js 将多种渲染模式统一到一个框架中,让开发者可以在页面级别选择最适合的渲染策略:
| 渲染模式 | 全称 | 适用场景 | SEO友好 | 首屏速度 |
|---|---|---|---|---|
| SSR | Server-Side Rendering | 动态内容、个性化页面 | ✅ 优秀 | ⚡ 中等 |
| SSG | Static Site Generation | 营销页面、文档、博客 | ✅ 完美 | 🚀 最快 |
| ISR | Incremental Static Regeneration | 周期性更新内容 | ✅ 优秀 | 🚀 快 |
| CSR | Client-Side Rendering | 高交互应用、管理后台 | ❌ 较差 | 🐢 慢 |
2. Next.js 路由系统架构
2.1 路由系统演进
Next.js 14.x 引入了 App Router(基于 React Server Components),与 Pages Router 共存。
2.2 文件路由映射规则
App Router 路由示例(Next.js 14+):
src/app/
├── (marketing)/ # 路由组(不影响URL)
│ ├── about/
│ │ └── page.tsx → /about
│ └── layout.tsx # 共享布局
├── blog/
│ ├── [slug]/ # 动态路由
│ │ └── page.tsx → /blog/post-1
│ └── page.tsx → /blog
├── shop/
│ ├── [[...slug]]/ # 捕获所有路由(可选)
│ │ └── page.tsx → /shop, /shop/a, /shop/a/b
│ └── [...slug]/ # 捕获所有路由(必需)
│ └── page.tsx → /shop/a, /shop/a/b (非/shop)
└── page.tsx → /
核心源码位置(Next.js 14.2.x):
- 路由匹配逻辑:
packages/next/src/server/app-render/app-render.tsx - 文件系统路由解析:
packages/next/src/server/dev/parse-component-info.ts
3. SSR(服务器端渲染)深度解析
3.1 SSR 工作原理
服务器在每次请求时动态生成 HTML,然后发送给客户端。
3.2 Pages Router 中的 SSR 实现
使用 getServerSideProps 在每次请求时获取数据:
// src/pages/product/[id].tsx
import { GetServerSideProps, GetServerSidePropsContext } from 'next'
// 产品数据类型定义
interface Product {
id: string
name: string
price: number
description: string
lastUpdated: string // 展示实时性
}
interface ProductPageProps {
product: Product
timestamp: string // 服务器渲染时间
}
/**
* 在每次请求时在服务器端执行
*
* 源码参考:
* - packages/next/src/server/render.tsx (renderToHTML)
* - packages/next/src/server/get-server-side-props.ts
*/
export const getServerSideProps: GetServerSideProps<ProductPageProps> = async (
context: GetServerSidePropsContext
) => {
const { id } = context.params || {}
try {
// 实时获取产品数据(包含库存、价格变动)
const response = await fetch(`https://api.example.com/products/${id}`, {
headers: {
// 可传递用户 Cookie 进行个性化请求
cookie: context.req.headers.cookie || ''
}
})
if (!response.ok) {
return {
notFound: true // 返回 404 页面
}
}
const product: Product = await response.json()
// 可以访问请求上下文(req/res)、cookies、query 参数
return {
props: {
product,
timestamp: new Date().toISOString() // 每次请求都会变化
},
// 可选:设置 HTTP 缓存头(CDN 缓存,但 Next.js 仍会重新渲染)
// headers: {
// 'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=30'
// }
}
} catch (error) {
return {
redirect: {
destination: '/error', // 出错时重定向
permanent: false
}
}
}
}
// React 组件(接收 props 进行渲染)
export default function ProductPage({ product, timestamp }: ProductPageProps) {
return (
<div>
<h1>{product.name}</h1>
<p>价格:¥{product.price}</p>
<p>{product.description}</p>
<small>服务器渲染时间:{new Date(timestamp).toLocaleString('zh-CN')}</small>
</div>
)
}
3.3 App Router 中的 SSR 实现
在 App Router 中,SSR 是默认行为(Server Components):
// src/app/product/[id]/page.tsx
// 文件即路由,无需额外配置
import { notFound } from 'next/navigation'
// 定义产品类型
interface Product {
id: string
name: string
price: number
stock: number
}
// 异步服务器组件(默认就是 SSR)
//
// 核心原理:
// - packages/next/src/server/app-render/app-render.tsx
// - renderToHTML() 将 React Server Components 流式渲染为 HTML
export default async function ProductPage({
params
}: {
params: Promise<{ id: string }>
}) {
// 在服务器端执行(每次请求都会运行)
const { id } = await params
// 直接 fetch 数据(自动去重、缓存可配置)
const res = await fetch(`https://api.example.com/products/${id}`, {
// 默认缓存策略(可配置)
// next: { revalidate: 0 } // 禁用缓存,纯 SSR
})
if (!res.ok) {
notFound() // 调用 notFound() 显示 404 页面
}
const product: Product = await res.json()
// 返回 JSX(在服务器端序列化为 HTML)
return (
<div>
<h1>{product.name}</h1>
<p>价格:¥{product.price}</p>
<p>库存:{product.stock} 件</p>
<small>渲染时间:{new Date().toLocaleString('zh-CN')}</small>
</div>
)
}
3.4 SSR 核心源码分析
渲染流程关键代码(Next.js 14.2.x):
// packages/next/src/server/render.tsx (简化版)
import { renderToReadableStream } from 'react-dom/server'
/**
* SSR 核心渲染函数
*
* 关键步骤:
* 1. 创建 React 组件树
* 2. 调用 getServerSideProps 或异步组件获取数据
* 3. 使用 renderToReadableStream 将组件流式渲染为 HTML
* 4. 返回完整 HTML + hydration 数据
*/
export async function renderToHTML({
pathname,
query,
req,
res
}: RenderOpts): Promise<RenderResult> {
// 1. 数据获取阶段
const props = await getServerSideProps({ req, res, query })
// 2. 渲染阶段(流式渲染)
const stream = await renderToReadableStream(
<AppRouter {...props} />,
{
// 启用 Suspense 流式渲染
onError(error) {
console.error('SSR Error:', error)
}
}
)
// 3. 等待流完成
await stream.allReady
// 4. 序列化 hydration 数据(嵌入到 HTML 中)
const hydrationData = JSON.stringify(props)
return {
html: stream,
hydrationData // 传递给客户端进行 hydrate
}
}
3.5 SSR 优缺点对比
| 维度 | 优势 | 劣势 |
|---|---|---|
| SEO | ✅ 完美,爬虫直接获取完整 HTML | - |
| 首屏速度 | ⚡ 快(服务器已渲染好) | 🐢 受服务器响应时间影响 |
| 数据新鲜度 | 🆕 每次请求都最新 | - |
| 服务器负载 | - | 📊 高(每次请求都计算) |
| 缓存难度 | - | ❌ 难(个性化内容无法 CDN 缓存) |
| 开发复杂度 | ⭐ 中等 | - |
4. SSG(静态站点生成)深度解析
4.1 SSG 工作原理
构建时(Build Time)预先生成静态 HTML 文件,部署后直接返回静态文件。
4.2 Pages Router 中的 SSG
使用 getStaticProps + getStaticPaths 实现静态生成:
// src/pages/blog/[slug].tsx
import { GetStaticProps, GetStaticPaths, GetStaticPropsContext } from 'next'
interface BlogPost {
id: string
title: string
content: string
author: string
publishedAt: string
}
interface BlogPageProps {
post: BlogPost
}
/**
* 构建时生成静态页面
*
* 源码参考:
* - packages/next/src/server/get-static-props.ts
* - packages/next/src/build.ts (generateStaticPages)
*/
export const getStaticProps: GetStaticProps<BlogPageProps> = async (
context: GetStaticPropsContext
) => {
const { slug } = context.params || {}
// 构建时获取文章数据(只执行一次)
const res = await fetch(`https://api.example.com/blog/${slug}`)
const post: BlogPost = await res.json()
return {
props: {
post
},
// 可选:启用 ISR(增量静态再生成)
revalidate: 60 // 每 60 秒允许重新生成一次
}
}
/**
* 指定哪些路径需要预渲染
*
* 构建时会为每个 path 调用 getStaticProps 生成 HTML
*/
export const getStaticPaths: GetStaticPaths = async () => {
// 构建时获取所有文章列表
const res = await fetch('https://api.example.com/blog')
const posts: BlogPost[] = await res.json()
// 返回需要预渲染的路径列表
const paths = posts.map((post) => ({
params: { slug: post.id }
}))
return {
paths,
fallback: false // false=只渲染这些路径, 404=true=首次访问时生成
}
}
export default function BlogPage({ post }: BlogPageProps) {
return (
<article>
<h1>{post.title}</h1>
<p>作者:{post.author}</p>
<time>发布于:{post.publishedAt}</time>
<div>{post.content}</div>
</article>
)
}
4.3 App Router 中的 SSG
在 App Router 中,SSG 通过 force-static 或默认缓存实现:
// src/app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'
interface BlogPost {
id: string
title: string
content: string
}
/**
* 静态生成的异步组件
*
* 关键配置:
* - fetch 的 next.revalidate 控制重新验证
* - Route Segment Config 控制整个页面
*/
export default async function BlogPostPage({
params
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
// 构建/重新验证时获取数据
const res = await fetch(`https://api.example.com/blog/${slug}`, {
next: {
revalidate: 3600 // 1 小时后允许重新生成(ISR)
}
})
if (!res.ok) {
notFound()
}
const post: BlogPost = await res.json()
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
)
}
/**
* 生成静态路径(可选)
*
* 源码:packages/next/src/server/app-render/generate-params.ts
*/
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/blog').then(r => r.json())
// 返回需要预渲染的路径
return posts.map((post: BlogPost) => ({
slug: post.id
}))
}
/**
* 路由段配置(强制静态生成)
*
* 文件:src/app/blog/[slug]/page.tsx (同级)
*/
export const dynamic = 'force-static' // 强制静态生成
// export const dynamicParams = false // 禁止动态参数(只预渲染的路径可访问)
4.4 SSG 构建流程源码分析
构建时静态生成关键代码(Next.js 14.2.x):
// packages/next/src/build.ts (简化版)
import { renderToStaticMarkup } from 'react-dom/server'
/**
* 构建时静态页面生成器
*
* 流程:
* 1. 扫描 pages/ 目录,识别所有页面
* 2. 对每个带 getStaticProps 的页面:
* a. 调用 getStaticPaths 获取路径列表
* b. 对每个路径调用 getStaticProps 获取数据
* c. 渲染 React 组件为静态 HTML
* 3. 将生成的 HTML 写入 .next/static/pages/
*/
export async function generateStaticPages(
pages: Page[],
config: BuildConfig
) {
const staticPages: Record<string, string> = {}
for (const page of pages) {
if (page.getStaticProps) {
// 1. 获取路径列表
const paths = await page.getStaticPaths()
// 2. 为每个路径生成 HTML
for (const path of paths) {
const props = await page.getStaticProps(path.params)
const html = renderToStaticMarkup(
<PageComponent {...props} />
)
// 3. 保存静态 HTML
staticPages[path.pathname] = html
}
}
}
return staticPages
}
4.5 SSG 优缺点对比
| 维度 | 优势 | 劣势 |
|---|---|---|
| 性能 | 🚀 最快(CDN 直接返回) | - |
| 服务器负载 | 📉 最低(无服务器计算) | - |
| 成本 | 💰 低(可纯 CDN 托管) | - |
| SEO | ✅ 完美 | - |
| 数据新鲜度 | - | ❌ 构建时数据(需重新构建) |
| 动态内容 | - | ❌ 不支持实时数据 |
5. ISR(增量静态再生成)混合模式
5.1 ISR 工作原理
ISR 结合了 SSG 的性能和 SSR 的灵活性:后台自动更新静态页面。
5.2 ISR 实现方式
Pages Router:
// src/pages/products/[id].tsx
export const getStaticProps: GetStaticProps = async (context) => {
const product = await fetchProduct(context.params.id)
return {
props: { product },
revalidate: 60 // ⭐ 关键:60 秒后允许重新生成
}
}
export const getStaticPaths: GetStaticPaths = async () => {
const products = await fetchAllProducts()
return {
paths: products.map(p => ({ params: { id: p.id } })),
fallback: 'blocking' // ⭐ 新路径:等待生成(而非 404)
}
}
App Router:
// src/app/products/[id]/page.tsx
export default async function ProductPage({ params }) {
const { id } = await params
const product = await fetch(`https://api.example.com/products/${id}`, {
next: {
revalidate: 60 // ⭐ ISR:60 秒后重新验证
}
}).then(r => r.json())
return <div>{product.name}</div>
}
5.3 ISR vs SSR vs SSG 对比
| 特性 | SSG | ISR | SSR |
|---|---|---|---|
| 生成时机 | 构建时 | 构建时 + 后台更新 | 每次请求 |
| 首屏速度 | 🚀 最快 | 🚀 快 | ⚡ 中等 |
| 数据新鲜度 | ❌ 构建时数据 | ✅ 定期更新 | ✅ 实时 |
| 服务器负载 | 📉 无 | 📊 低(仅后台更新) | 📈 高(每次请求) |
| 适用场景 | 文档、博客 | 新闻、电商列表 | 个性化页面、实时数据 |
6. 渲染模式选择指南
6.1 决策流程图
6.2 实际应用场景矩阵
| 场景 | 推荐模式 | 理由 | 配置示例 |
|---|---|---|---|
| 公司官网 | SSG | 内容不变,追求极致性能 | export const dynamic = 'force-static' |
| 技术博客 | ISR | 文章定期更新,评论可动态加载 | next: { revalidate: 3600 } |
| 电商首页 | ISR | 商品列表定时更新 | revalidate: 300 |
| 商品详情页 | SSR | 库存、价格实时变化 | 默认 Server Component |
| 用户中心 | SSR | 高度个性化数据 | getServerSideProps 或 默认 |
| 管理后台 | CSR | 交互复杂,无需 SEO | 'use client' + 客户端数据获取 |
| API 文档 | SSG | 纯静态内容 | 构建时生成 |
6.3 混合渲染策略
Next.js 允许在同一应用中混合使用多种渲染模式:
// src/app/layout.tsx
export default function RootLayout({ children }) {
return (
<html>
<body>
<Header /> {/* SSG:静态导航 */}
<main>{children}</main>
<Footer /> {/* SSG:静态页脚 */}
</body>
</html>
)
}
// src/app/page.tsx (SSG)
export const dynamic = 'force-static'
export default function HomePage() { /* ... */ }
// src/app/dashboard/page.tsx (SSR)
export default async function DashboardPage() {
const session = await getSession() // 实时用户数据
return <div>Welcome, {session.user.name}</div>
}
// src/app/products/page.tsx (ISR)
export default async function ProductsPage() {
const products = await fetch('...', {
next: { revalidate: 60 }
}).then(r => r.json())
return <ProductList products={products} />
}
7. 性能优化实战
7.1 缓存策略优化
多层缓存架构:
代码实现:
// src/app/product/[id]/page.tsx
export const dynamic = 'force-dynamic' // 禁用 Next.js 缓存
export default async function ProductPage({ params }) {
const { id } = await params
// 多层缓存策略
const product = await fetch(`https://api.example.com/products/${id}`, {
// 1. 浏览器缓存:5 分钟
headers: {
'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600'
},
// 2. Next.js 数据缓存:60 秒
next: {
revalidate: 60
}
// 3. CDN 缓存:由 Cache-Control 控制
}).then(r => r.json())
return <div>{product.name}</div>
}
// Route Segment Config(页面级配置)
export const fetchCache = 'force-cache' // 强制缓存所有 fetch
export const revalidate = 120 // 覆盖 fetch 的 revalidate
7.2 流式渲染(Streaming)
核心原理:先发送页面骨架,再异步填充数据。
// src/app/dashboard/page.tsx
import { Suspense } from 'react'
// 快速加载的骨架组件
function DashboardSkeleton() {
return (
<div>
<Skeleton height={40} width={200} />
<Skeleton count={5} />
</div>
)
}
// 慢速组件(后台数据)
async function SlowWidget() {
const data = await fetchData() // 3 秒
return <div>{data}</div>
}
// 快速组件(缓存数据)
async function FastWidget() {
const data = await fetch('...', {
next: { revalidate: 60 } // 使用缓存
})
return <div>{data}</div>
}
export default function DashboardPage() {
return (
<div>
<h1>仪表盘</h1>
{/* 快速加载 */}
<FastWidget />
{/* 延迟加载,显示骨架屏 */}
<Suspense fallback={<DashboardSkeleton />}>
<SlowWidget />
</Suspense>
</div>
)
}
流式渲染源码机制(Next.js 14+):
// packages/next/src/server/app-render/app-render.tsx
import { renderToReadableStream } from 'react-dom/server'
/**
* 流式渲染实现
*
* 原理:
* 1. 使用 React 18 的 renderToReadableStream
* 2. 遇到 Suspense 边界时,先发送 fallback
* 3. 数据返回后,通过流式传输发送真实内容
*/
export async function renderApp(opts: RenderOpts) {
const stream = await renderToReadableStream(
<App />,
{
// 启用流式传输
onError(error) {
console.error('Streaming error:', error)
}
}
)
// 返回 ReadableStream(支持 Transfer Encoding: chunked)
return stream
}
7.3 性能监控指标
| 指标 | 定义 | 目标值 | 优化方式 |
|---|---|---|---|
| TTFB | 首字节时间 | < 600ms | CDN、SSG |
| FCP | 首次内容绘制 | < 1.8s | SSG、预加载资源 |
| LCP | 最大内容绘制 | < 2.5s | 图片优化、骨架屏 |
| TTI | 可交互时间 | < 3.8s | 代码分割、懒加载 |
| CLS | 累积布局偏移 | < 0.1 | 图片尺寸预留 |
8. 总结
8.1 核心要点
- Next.js 渲染模式演进:从单一的 SSR/SSG 到混合渲染(ISR),再到流式渲染和部分渲染
- 路由系统架构:Pages Router(传统)和 App Router(RSC、嵌套布局)并存
- SSR 适用场景:实时数据、个性化内容、SEO 要求高
- SSG 适用场景:静态内容、文档、营销页面
- ISR 平衡之道:结合 SSG 性能和动态更新能力
- 性能优化核心:多层缓存、流式渲染、按需渲染
8.2 版本演进路线
8.3 源码学习路径
| 模块 | 文件路径 | 学习重点 |
|---|---|---|
| 路由解析 | packages/next/src/server/dev/parse-component-info.ts |
文件系统到路由映射 |
| SSR 渲染 | packages/next/src/server/render.tsx |
renderToHTML、hydration |
| SSG 生成 | packages/next/src/build.ts |
generateStaticPages |
| App Router | packages/next/src/server/app-render/ |
RSC 渲染、嵌套布局 |
| 缓存机制 | packages/next/src/server/response-cache/ |
数据缓存、ISR |
8.4 最佳实践清单
- ✅ 优先使用 SSG:如果数据允许,静态生成性能最佳
- ✅ ISR 折中方案:需要周期性更新时使用 ISR
- ✅ SSR 按需使用:个性化内容、实时数据
- ✅ 混合渲染:在同一应用中灵活组合不同模式
- ✅ 缓存分层:CDN + Next.js + 数据源多层缓存
- ✅ 流式渲染:使用 Suspense 提升首屏体验
- ✅ 监控指标:持续关注 Core Web Vitals
参考资料
🎯 关注我,获取更多前端深度解析文章!
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)