Next.js SSR/SSG:路由与渲染模式深度解析

版本说明:本文基于 Next.js 14.x 和 15.x 最新版本编写,源码路径参考 packages/next/src/ 核心模块


📑 目录

  1. 引言:渲染模式的演进
  2. Next.js 路由系统架构
  3. SSR(服务器端渲染)深度解析
  4. SSG(静态站点生成)深度解析
  5. ISR(增量静态再生成)混合模式
  6. 渲染模式选择指南
  7. 性能优化实战
  8. 总结

1. 引言:渲染模式的演进

1.1 Web 渲染简史

从传统的服务端渲染(PHP、JSP)到客户端渲染(SPA),再到现代的混合渲染模式,Web 开发经历了一个螺旋式上升的过程。

1990s 服务端渲染 (PHP/JSP/ASP) 每次请求服务器生成完整HTML 2000s AJAX 出现 局部动态更新,用户体验提升 2010s 单页应用 (SPA) React/Vue/Angular,完全客户端渲染 2015s 同构渲染 (Universal SSR) React SSR,首次服务器渲染,后续客户端接管 2019s 混合渲染时代 (Next.js) SSR/SSG/ISR 按需组合 2024s 边缘渲染与部分渲染 RSC/Streaming/PPR 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 共存。

Next.js 应用

Pages Router
src/pages/

App Router
src/app/

文件系统路由

getServerSideProps
SSR

getStaticProps
SSG/ISR

嵌套布局

Server Components
默认

Client Components
use client

数据获取方法
异步组件

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,然后发送给客户端。

🗄️ 数据库/API 🖥️ Next.js 服务器 👤 用户浏览器 🗄️ 数据库/API 🖥️ Next.js 服务器 👤 用户浏览器 1. 请求页面 2. 获取数据(每次都请求) 3. 返回最新数据 4. 渲染 React 组件 → HTML 5. 返回完整 HTML 6. 显示内容(首屏快) 7. 加载 JS,hydrate(可交互)

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 文件,部署后直接返回静态文件。

👤 用户 🖥️ 生产服务器 🔨 Next.js 构建 👨‍💻 开发者 👤 用户 🖥️ 生产服务器 🔨 Next.js 构建 👨‍💻 开发者 生产环境 1. 运行 next build 2. 执行 getStaticProps 3. 生成所有路径的 HTML 4. 部署静态文件 5. 请求页面 6. 立即返回静态 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 的灵活性:后台自动更新静态页面。

⚙️ 后台更新任务 🖥️ Next.js 服务器 🌐 CDN 缓存 👤 用户 ⚙️ 后台更新任务 🖥️ Next.js 服务器 🌐 CDN 缓存 👤 用户 检查缓存是否过期 下一个用户获得新页面 alt [缓存过期 (revalidate 时间到)] 1. 请求页面 2. 返回缓存的静态页面(快) 3. 触发后台重新生成 4. 执行 getStaticProps 获取新数据 5. 返回新的 HTML(更新缓存)

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 决策流程图

几乎不变

周期性变化

选择渲染模式

数据是否需要
实时更新?

页面是否
用户个性化?

选择 SSR

数据变化频率?

选择 SSG
构建时生成

选择 ISR
后台更新

示例: 营销页、文档

示例: 新闻、博客

示例: 用户中心、实时数据

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 缓存策略优化

多层缓存架构

命中

未命中

命中

未命中

用户请求

CDN 缓存

返回缓存 HTML

Next.js 服务器

Data Cache

使用缓存数据渲染

请求 API

渲染 HTML

更新 CDN 缓存

代码实现

// 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 核心要点

  1. Next.js 渲染模式演进:从单一的 SSR/SSG 到混合渲染(ISR),再到流式渲染和部分渲染
  2. 路由系统架构:Pages Router(传统)和 App Router(RSC、嵌套布局)并存
  3. SSR 适用场景:实时数据、个性化内容、SEO 要求高
  4. SSG 适用场景:静态内容、文档、营销页面
  5. ISR 平衡之道:结合 SSG 性能和动态更新能力
  6. 性能优化核心:多层缓存、流式渲染、按需渲染

8.2 版本演进路线

Next.js 9-10
基础 SSR/SSG

Next.js 11-12
ISR + fallback

Next.js 13
App Router Beta

Next.js 14
App Router 稳定
Server Components

Next.js 15
部分预渲染 PPR
Turbopack 默认

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

参考资料


🎯 关注我,获取更多前端深度解析文章!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐