服务端渲染 (SSR) 是什么?Next.js 做了什么?

30 阅读5分钟

服务端渲染 (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 开发:

  1. 开箱即用的 SSR:无需复杂配置即可使用
  2. 多种渲染策略:SSR、SSG、ISR 灵活选择
  3. 性能优化:自动代码分割、图片优化、字体优化
  4. 开发体验:热重载、TypeScript 支持、ESLint 集成
  5. 部署优化:Vercel 平台深度集成

选择 SSR 时需要考虑:

  • SEO 需求:是否需要搜索引擎优化
  • 首屏性能:用户对加载速度的要求
  • 服务器资源:是否有足够的服务器资源
  • 开发复杂度:团队的技术能力和维护成本

Next.js 通过提供完整的 SSR 解决方案,大大降低了 SSR 的开发门槛,是现代 React 应用的首选框架之一。