Skip to content

Next.js 服务端渲染 (SSR) 🖥️

🎯 概述

服务端渲染(Server-Side Rendering,SSR)是 Next.js 的核心特性之一,它在服务器上生成 HTML,然后发送到客户端,提供更好的 SEO 和首屏加载性能。

📦 什么是 SSR?

SSR 工作原理

  1. 用户请求页面
  2. 服务器执行 React 组件
  3. 生成完整的 HTML
  4. 发送 HTML 到客户端
  5. 客户端激活(Hydration)

SSR vs CSR

特性SSRCSR
首屏加载
SEO优秀较差
服务器负载
交互延迟

🚀 App Router 中的 SSR

Server Components(默认)

tsx
// app/page.tsx
// 默认就是服务端组件,自动 SSR
async function getData() {
  const res = await fetch('https://api.example.com/data')
  return res.json()
}

export default async function Page() {
  const data = await getData()
  
  return (
    <div>
      <h1>{data.title}</h1>
      <p>{data.content}</p>
    </div>
  )
}

动态渲染

tsx
// app/posts/[id]/page.tsx
async function getPost(id: string) {
  const res = await fetch(`https://api.example.com/posts/${id}`, {
    cache: 'no-store' // 每次请求都重新获取
  })
  return res.json()
}

export default async function Post({ params }: { params: { id: string } }) {
  const post = await getPost(params.id)
  
  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  )
}

📝 Pages Router 中的 SSR

getServerSideProps

tsx
// pages/posts/[id].tsx
import { GetServerSideProps } from 'next'

interface Post {
  id: string
  title: string
  content: string
}

interface Props {
  post: Post
}

export const getServerSideProps: GetServerSideProps<Props> = async (context) => {
  const { id } = context.params!
  
  const res = await fetch(`https://api.example.com/posts/${id}`)
  const post = await res.json()
  
  return {
    props: {
      post,
    },
  }
}

export default function Post({ post }: Props) {
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  )
}

访问请求信息

tsx
export const getServerSideProps: GetServerSideProps = async (context) => {
  const { req, res, query, params } = context
  
  // 获取 cookies
  const cookies = req.cookies
  
  // 获取请求头
  const userAgent = req.headers['user-agent']
  
  // 获取查询参数
  const searchQuery = query.q
  
  return {
    props: {
      userAgent,
      searchQuery,
    },
  }
}

🎨 数据获取模式

并行数据获取

tsx
// app/dashboard/page.tsx
async function getUser() {
  const res = await fetch('https://api.example.com/user')
  return res.json()
}

async function getPosts() {
  const res = await fetch('https://api.example.com/posts')
  return res.json()
}

export default async function Dashboard() {
  // 并行获取数据
  const [user, posts] = await Promise.all([
    getUser(),
    getPosts(),
  ])
  
  return (
    <div>
      <h1>欢迎,{user.name}</h1>
      <div>
        {posts.map(post => (
          <article key={post.id}>{post.title}</article>
        ))}
      </div>
    </div>
  )
}

串行数据获取

tsx
// app/profile/page.tsx
async function getUser(id: string) {
  const res = await fetch(`https://api.example.com/users/${id}`)
  return res.json()
}

async function getUserPosts(userId: string) {
  const res = await fetch(`https://api.example.com/users/${userId}/posts`)
  return res.json()
}

export default async function Profile() {
  // 先获取用户信息
  const user = await getUser('123')
  
  // 再获取用户的文章
  const posts = await getUserPosts(user.id)
  
  return (
    <div>
      <h1>{user.name}</h1>
      <div>
        {posts.map(post => (
          <article key={post.id}>{post.title}</article>
        ))}
      </div>
    </div>
  )
}

🔄 缓存策略

强制动态渲染

tsx
// app/page.tsx
export const dynamic = 'force-dynamic'

export default async function Page() {
  const data = await fetch('https://api.example.com/data')
  return <div>{/* ... */}</div>
}

重新验证

tsx
// 每 60 秒重新验证一次
async function getData() {
  const res = await fetch('https://api.example.com/data', {
    next: { revalidate: 60 }
  })
  return res.json()
}

禁用缓存

tsx
async function getData() {
  const res = await fetch('https://api.example.com/data', {
    cache: 'no-store'
  })
  return res.json()
}

📊 性能优化

1. 使用 Streaming

tsx
// app/page.tsx
import { Suspense } from 'react'

async function SlowComponent() {
  await new Promise(resolve => setTimeout(resolve, 3000))
  return <div>慢速组件</div>
}

export default function Page() {
  return (
    <div>
      <h1>快速内容</h1>
      <Suspense fallback={<div>加载中...</div>}>
        <SlowComponent />
      </Suspense>
    </div>
  )
}

2. 数据预取

tsx
// app/posts/page.tsx
import Link from 'next/link'

async function getPosts() {
  const res = await fetch('https://api.example.com/posts')
  return res.json()
}

export default async function Posts() {
  const posts = await getPosts()
  
  return (
    <div>
      {posts.map(post => (
        <Link 
          key={post.id} 
          href={`/posts/${post.id}`}
          prefetch={true} // 预取链接页面
        >
          {post.title}
        </Link>
      ))}
    </div>
  )
}

3. 部分预渲染

tsx
// app/page.tsx
export const experimental_ppr = true

export default function Page() {
  return (
    <div>
      <StaticHeader />
      <Suspense fallback={<Skeleton />}>
        <DynamicContent />
      </Suspense>
    </div>
  )
}

🎯 实战示例

博客文章页面

tsx
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'

interface Post {
  slug: string
  title: string
  content: string
  author: string
  publishedAt: string
}

async function getPost(slug: string): Promise<Post | null> {
  const res = await fetch(`https://api.example.com/posts/${slug}`, {
    next: { revalidate: 3600 } // 1小时重新验证
  })
  
  if (!res.ok) return null
  return res.json()
}

export async function generateMetadata({ params }) {
  const post = await getPost(params.slug)
  
  if (!post) return {}
  
  return {
    title: post.title,
    description: post.content.substring(0, 160),
  }
}

export default async function BlogPost({ params }) {
  const post = await getPost(params.slug)
  
  if (!post) {
    notFound()
  }
  
  return (
    <article>
      <h1>{post.title}</h1>
      <p>作者:{post.author}</p>
      <time>{post.publishedAt}</time>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

用户仪表板

tsx
// app/dashboard/page.tsx
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'

async function getUser(token: string) {
  const res = await fetch('https://api.example.com/user', {
    headers: {
      Authorization: `Bearer ${token}`,
    },
    cache: 'no-store',
  })
  
  if (!res.ok) return null
  return res.json()
}

export default async function Dashboard() {
  const cookieStore = cookies()
  const token = cookieStore.get('token')?.value
  
  if (!token) {
    redirect('/login')
  }
  
  const user = await getUser(token)
  
  if (!user) {
    redirect('/login')
  }
  
  return (
    <div>
      <h1>欢迎,{user.name}</h1>
      <p>邮箱:{user.email}</p>
    </div>
  )
}

📚 最佳实践

1. 合理使用缓存

tsx
// 静态内容 - 长时间缓存
const staticData = await fetch('url', {
  next: { revalidate: 86400 } // 24小时
})

// 动态内容 - 短时间缓存
const dynamicData = await fetch('url', {
  next: { revalidate: 60 } // 1分钟
})

// 实时内容 - 不缓存
const realtimeData = await fetch('url', {
  cache: 'no-store'
})

2. 错误处理

tsx
async function getData() {
  try {
    const res = await fetch('https://api.example.com/data')
    if (!res.ok) throw new Error('获取失败')
    return res.json()
  } catch (error) {
    console.error('数据获取错误:', error)
    return null
  }
}

export default async function Page() {
  const data = await getData()
  
  if (!data) {
    return <div>加载失败</div>
  }
  
  return <div>{data.title}</div>
}

3. 避免客户端状态

tsx
// ❌ 不推荐:在服务端组件中使用客户端状态
export default async function Page() {
  const [count, setCount] = useState(0) // 错误!
  return <div>{count}</div>
}

// ✅ 推荐:使用客户端组件
'use client'
export default function Counter() {
  const [count, setCount] = useState(0)
  return <div>{count}</div>
}

🔗 相关资源


下一步:学习 Next.js 静态生成 (SSG)