#Next.js Server-Side Rendering (SSR)
#Overview
Server-Side Rendering (SSR) is one of Next.js's core features. It generates HTML on the server and sends it to the client, providing better SEO and first-screen loading performance.
#What is SSR?
#How SSR Works
- User requests a page
- The server executes React components
- Complete HTML is generated
- HTML is sent to the client
- Client-side hydration activates the page
#SSR vs CSR
| Feature | SSR | CSR |
|---|---|---|
| First-screen load | Fast | Slow |
| SEO | Excellent | Poor |
| Server load | High | Low |
| Interaction latency | Low | High |
#SSR in the App Router
#Server Components (Default)
// 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>
)
}#Dynamic Rendering
// 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>
)
}#SSR in the Pages Router
#getServerSideProps
// 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>
)
}#Accessing Request Information
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,
},
}
}#Data Fetching Patterns
#Parallel Data Fetching
// 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>
)
}#Sequential Data Fetching
// 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>
)
}#Caching Strategies
#Force Dynamic Rendering
// 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>
}#Revalidation
// 每 60 秒重新验证一次
async function getData() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 60 }
})
return res.json()
}#Disable Caching
async function getData() {
const res = await fetch('https://api.example.com/data', {
cache: 'no-store'
})
return res.json()
}#Performance Optimization
#1. Using Streaming
// 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. Data Prefetching
// 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. Partial Pre-Rendering
// app/page.tsx
export const experimental_ppr = true
export default function Page() {
return (
<div>
<StaticHeader />
<Suspense fallback={<Skeleton />}>
<DynamicContent />
</Suspense>
</div>
)
}#Practical Examples
#Blog Post Page
// 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>
)
}#User Dashboard
// 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>
)
}#Best Practices
#1. Use Caching Appropriately
// 静态内容 - 长时间缓存
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. Error Handling
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. Avoid Client State in Server Components
// ❌ 不推荐:在服务端组件中使用客户端状态
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>
}#Related Resources
Previous Chapter: Error Handling | Next Chapter: Static Site Generation (SSG)