服务端渲染 (SSR) 是什么?Next.js 做了什么?
服务端渲染 (SSR) 概述
服务端渲染 (Server-Side Rendering, SSR) 是一种在服务器端生成完整 HTML 页面的技术,然后将渲染好的页面发送给客户端。这与传统的客户端渲染 (CSR) 形成对比。
传统客户端渲染 vs 服务端渲染
// 客户端渲染 (CSR) - React 默认方式
// 1. 服务器返回空的 HTML 和 JavaScript bundle
// 2. 浏览器下载并执行 JavaScript
// 3. React 在客户端渲染组件
// 4. 用户看到内容
// 服务端渲染 (SSR)
// 1. 服务器执行 React 组件
// 2. 生成完整的 HTML
// 3. 发送 HTML 给客户端
// 4. 客户端接管 (hydration)
SSR 的优势
1. 更好的 SEO
// CSR 问题:搜索引擎爬虫看到的是空页面
// <div id="root"></div>
// SSR 优势:搜索引擎看到完整内容
// <div id="root">
// <h1>我的博客</h1>
// <article>完整的文章内容...</article>
// </div>
2. 更快的首屏加载
// CSR:需要等待 JavaScript 下载和执行
// 时间线:HTML → JS Bundle → React 渲染 → 内容显示
// SSR:立即显示内容
// 时间线:HTML with Content → 立即显示 → JS Hydration
3. 更好的用户体验
// 特别是在慢网络环境下
// CSR:白屏时间较长
// SSR:立即看到内容,然后逐步增强
SSR 的挑战
1. 服务器负载
// 每个请求都需要服务器渲染
// 需要更多的服务器资源
// 需要考虑缓存策略
2. 复杂性增加
// 需要考虑服务端和客户端的差异
// 某些 API 只在客户端可用
// 状态同步问题
3. 开发复杂度
// 需要处理服务端和客户端的环境差异
// 构建配置更复杂
// 调试更困难
Next.js 的 SSR 实现
1. 页面级 SSR (getServerSideProps)
// pages/posts/[id].js
import { GetServerSideProps } from 'next'
function Post({ post }) {
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
)
}
// 服务端数据获取
export const getServerSideProps = 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 Post
2. 静态生成 (getStaticProps)
// pages/posts/[id].js
import { GetStaticProps, GetStaticPaths } from 'next'
function Post({ post }) {
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
)
}
// 构建时生成静态页面
export const getStaticProps = async ({ params }) => {
const res = await fetch(`https://api.example.com/posts/${params.id}`)
const post = await res.json()
return {
props: {
post,
},
revalidate: 60, // ISR: 60秒后重新生成
}
}
// 定义动态路由的路径
export const getStaticPaths = async () => {
const res = await fetch('https://api.example.com/posts')
const posts = await res.json()
const paths = posts.map((post) => ({
params: { id: post.id.toString() },
}))
return {
paths,
fallback: 'blocking', // 或 true, false
}
}
export default Post
3. 增量静态再生 (ISR)
// pages/blog/[slug].js
export const getStaticProps = async ({ params }) => {
const post = await fetchPost(params.slug)
return {
props: {
post,
},
revalidate: 3600, // 1小时后重新生成
}
}
// 首次访问:返回静态页面
// 1小时后访问:后台重新生成,下次访问返回新页面
// 构建时:只生成部分页面,其他页面按需生成
Next.js 的核心特性
1. 文件系统路由
// pages/index.js → /
// pages/about.js → /about
// pages/blog/[slug].js → /blog/hello-world
// pages/api/users.js → /api/users
// 自动代码分割
// 每个页面只加载必要的代码
2. API Routes
// pages/api/users.js
export default function handler(req, res) {
if (req.method === 'GET') {
res.status(200).json({ users: [] })
} else if (req.method === 'POST') {
// 处理 POST 请求
res.status(201).json({ message: 'User created' })
}
}
// 支持中间件
export const config = {
api: {
bodyParser: {
sizeLimit: '1mb',
},
},
}
3. 图片优化
import Image from 'next/image'
function MyComponent() {
return (
<Image
src="/hero.jpg"
alt="Hero image"
width={800}
height={600}
priority // 优先加载
placeholder="blur" // 模糊占位符
blurDataURL="data:image/jpeg;base64,..."
/>
)
}
// 自动优化:
// - WebP 格式转换
// - 响应式图片
// - 懒加载
// - 防止布局偏移
4. 字体优化
// pages/_document.js
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html>
<Head>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
rel="stylesheet"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
// 或者使用 next/font
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
export default function MyApp({ Component, pageProps }) {
return (
<main className={inter.className}>
<Component {...pageProps} />
</main>
)
}
高级 SSR 模式
1. 流式 SSR
// React 18 的流式 SSR
import { renderToPipeableStream } from 'react-dom/server'
app.get('*', (req, res) => {
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
res.setHeader('content-type', 'text/html')
pipe(res)
},
})
})
2. 选择性水合
// 优先水合重要组件
import { Suspense } from 'react'
function App() {
return (
<div>
<Header />
<Suspense fallback={<div>Loading...</div>}>
<MainContent />
</Suspense>
<Footer />
</div>
)
}
3. 部分预渲染
// 结合静态和动态内容
export const getStaticProps = async () => {
return {
props: {
staticData: await fetchStaticData(),
},
}
}
function Page({ staticData }) {
return (
<div>
<StaticContent data={staticData} />
<Suspense fallback={<div>Loading...</div>}>
<DynamicContent />
</Suspense>
</div>
)
}
性能优化策略
1. 缓存策略
// 页面级缓存
export const getServerSideProps = async () => {
const data = await fetchData()
return {
props: { data },
// 缓存 60 秒
headers: {
'Cache-Control': 's-maxage=60, stale-while-revalidate',
},
}
}
// API 路由缓存
export default function handler(req, res) {
res.setHeader('Cache-Control', 's-maxage=86400, stale-while-revalidate')
res.json({ data: 'cached data' })
}
2. 代码分割
// 动态导入
import dynamic from 'next/dynamic'
const HeavyComponent = dynamic(() => import('../components/Heavy'), {
loading: () => <p>Loading...</p>,
ssr: false, // 禁用 SSR
})
// 路由级代码分割
// Next.js 自动处理
3. 预加载策略
// 预加载关键资源
import Head from 'next/head'
function MyPage() {
return (
<Head>
<link rel="preload" href="/api/critical-data" as="fetch" />
<link rel="prefetch" href="/next-page" />
</Head>
)
}
错误处理
1. 错误边界
// pages/_error.js
function Error({ statusCode }) {
return (
<p>
{statusCode
? `An error ${statusCode} occurred on server`
: 'An error occurred on client'}
</p>
)
}
Error.getInitialProps = ({ res, err }) => {
const statusCode = res ? res.statusCode : err ? err.statusCode : 404
return { statusCode }
}
export default Error
2. API 错误处理
// pages/api/users.js
export default async function handler(req, res) {
try {
const users = await fetchUsers()
res.status(200).json(users)
} catch (error) {
console.error('API Error:', error)
res.status(500).json({ error: 'Internal Server Error' })
}
}
部署和监控
1. Vercel 部署
// vercel.json
{
"functions": {
"pages/api/**/*.js": {
"maxDuration": 30
}
},
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "X-Content-Type-Options",
"value": "nosniff"
}
]
}
]
}
2. 性能监控
// pages/_app.js
import { Analytics } from '@vercel/analytics/react'
export default function App({ Component, pageProps }) {
return (
<>
<Component {...pageProps} />
<Analytics />
</>
)
}
// 自定义性能监控
export function reportWebVitals(metric) {
console.log(metric)
// 发送到分析服务
}
最佳实践
1. 数据获取策略
// 选择合适的渲染方式
// 静态内容:getStaticProps
// 动态内容:getServerSideProps
// 客户端数据:useEffect + useState
// 避免在 getServerSideProps 中调用内部 API
// 直接调用数据库或外部 API
2. SEO 优化
// pages/_document.js
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html lang="zh-CN">
<Head>
<meta name="description" content="页面描述" />
<meta property="og:title" content="页面标题" />
<meta property="og:description" content="页面描述" />
<meta property="og:image" content="/og-image.jpg" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
3. 安全考虑
// 环境变量
// .env.local
DATABASE_URL = your_database_url
SECRET_KEY = your_secret_key
// 只在服务端访问敏感数据
export const getServerSideProps = async () => {
const secretData = process.env.SECRET_KEY
// 处理数据...
return {
props: {
publicData: 'safe to expose',
},
}
}
总结
Next.js 通过以下方式简化了 SSR 开发:
- 开箱即用的 SSR:无需复杂配置即可使用
- 多种渲染策略:SSR、SSG、ISR 灵活选择
- 性能优化:自动代码分割、图片优化、字体优化
- 开发体验:热重载、TypeScript 支持、ESLint 集成
- 部署优化:Vercel 平台深度集成
选择 SSR 时需要考虑:
- SEO 需求:是否需要搜索引擎优化
- 首屏性能:用户对加载速度的要求
- 服务器资源:是否有足够的服务器资源
- 开发复杂度:团队的技术能力和维护成本
Next.js 通过提供完整的 SSR 解决方案,大大降低了 SSR 的开发门槛,是现代 React 应用的首选框架之一。