Next.js SSG 与同类方案怎么选:实测对比与选型指南
静态站点生成(SSG)是前端工程化中一个成熟但常被误用的技术。Next.js 的 SSG 能力并非孤立存在,它与 Gatsby、Hugo、Jekyll 等方案在构建速度、动态内容支持、部署复杂度上存在显著差异。本文从实战角度出发,通过多维对比和具体配置示例,帮你快速判断在什么场景下该选谁,以及如何避免踩坑。
它解决什么问题 / 适用场景
SSG 的核心价值在于:在构建时预渲染页面为静态 HTML,部署后无需服务端处理即可直接响应请求。这带来了极致的首屏加载速度和 CDN 缓存效率。
最适合的场景:
- 内容不频繁变化的网站:营销页面、博客文章、作品集、电商产品列表、帮助文档
- 与无头 CMS(如 Contentful、Sanity、Strapi)配合使用,在构建时获取数据并生成静态 HTML
- 需要高 SEO 评分且对动态内容要求低的页面
不适合的场景:
- 需要频繁更新数据的页面(如实时仪表盘、社交信息流)
- 用户个性化内容(如登录后的个人主页)
- 需要实时交互的复杂应用(如在线编辑器)
在这些场景下,应考虑客户端数据获取(useEffect + fetch)或服务端渲染(SSR)。
安装与快速上手
使用 Next.js 创建 SSG 项目非常简单。以下命令会创建一个带 TypeScript、ESLint、Tailwind CSS、src/ 目录和 App Router 的样板项目:
BASHnpx create-next-app@latest my-static-site --typescript --eslint --src-dir --app-router
在项目根目录下,创建 src/app/page.tsx 文件,写入以下代码即可实现一个最简单的 SSG 页面:
TSX// src/app/page.tsx export default async function HomePage() { // 在构建时获取数据 const res = await fetch('https://jsonplaceholder.typicode.com/posts'); const posts = await res.json(); return ( <div> <h1>Static Posts</h1> <ul> {posts.slice(0, 10).map((post: { id: number; title: string }) => ( <li key={post.id}>{post.title}</li> ))} </ul> </div> ); }
运行 npm run build 后,所有页面将被预渲染为静态 HTML 文件,输出到 .next 目录。
核心配置 / 参数说明
Next.js SSG 的核心在于 getStaticProps 和 getStaticPaths 两个函数(Pages Router)或 App Router 下的 generateStaticParams。以下是关键参数说明:
| 参数/函数 | 所属路由 | 用途 | 必填 | 说明 |
|---|---|---|---|---|
getStaticProps | Pages Router | 在构建时获取数据并注入页面 props | 是(SSG 页面) | 运行在 Node.js 环境,可访问文件系统、数据库等 |
getStaticPaths | Pages Router | 为动态路由(如 [id].js)生成所有可能的路径 | 是(动态 SSG 页面) | 返回 { paths: [], fallback: false/true/'blocking' } |
generateStaticParams | App Router | 替代 getStaticPaths,为动态路由生成路径 | 是(动态 SSG 页面) | 返回一个对象数组,每个对象对应一个路径参数 |
revalidate | 两者 | 启用增量静态再生(ISR),设置秒级重新验证间隔 | 否 | 如 revalidate: 60 表示每 60 秒后台重新生成一次 |
fallback | Pages Router | 控制未预渲染路径的访问行为 | 是(动态 SSG 页面) | false:返回 404;true:客户端回退;'blocking':服务端回退 |
示例:动态路由 SSG 页面(Pages Router)
JSX// pages/posts/[id].js export async function getStaticPaths() { const res = await fetch('https://jsonplaceholder.typicode.com/posts'); const posts = await res.json(); const paths = posts.map((post) => ({ params: { id: String(post.id) }, // 注意:id 必须是字符串 })); return { paths, fallback: 'blocking' }; } export async function getStaticProps({ params }) { const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${params.id}`); const post = await res.json(); return { props: { post }, revalidate: 60, // 启用 ISR,每 60 秒重新生成 }; }
与同类方案对比
以下表格从 6 个关键维度对比 Next.js SSG、Gatsby、Hugo 和 Jekyll:
| 维度 | Next.js SSG | Gatsby | Hugo | Jekyll |
|---|---|---|---|---|
| 构建速度 | 中等(大型站点需优化) | 慢(依赖 GraphQL 层) | 极快(Go 语言编写) | 快(Ruby 编写) |
| 部署复杂度 | 低(Vercel/Netlify 一键部署) | 中(需配置 Gatsby Cloud 或自建) | 低(纯静态文件) | 低(纯静态文件) |
| SEO 友好度 | 极高(预渲染 + 元数据 API) | 极高(插件生态丰富) | 极高(原生支持) | 高(需手动配置) |
| 动态内容支持 | 强(混合渲染:SSG + SSR + ISR) | 弱(需插件或客户端渲染) | 无(纯静态) | 无(纯静态) |
| CDN 缓存效率 | 极高(静态文件 + ISR 缓存) | 高(静态文件) | 极高(纯静态文件) | 极高(纯静态文件) |
| 增量构建支持 | 是(ISR / 按需 ISR) | 是(增量构建,但较慢) | 是(Hugo 0.112+) | 否(需全量构建) |
选型建议:
- 需要混合渲染能力(同一应用同时使用 SSG、SSR、ISR):选 Next.js。这是其核心差异化优势。
- 纯博客/文档站点,追求极致构建速度:选 Hugo。构建速度比 Next.js 快 10-100 倍。
- 依赖 GraphQL 数据层,团队熟悉 React:选 Gatsby。但注意其构建性能瓶颈。
- 简单静态站点,团队熟悉 Ruby:选 Jekyll。GitHub Pages 原生支持。
生产环境实践与注意事项
1. 构建时间优化
对于大型站点(如数千个页面),构建时间可能很长。以下策略可缓解:
- 使用 ISR:在
getStaticProps中设置revalidate,让页面在首次请求时生成,而非全部在构建时生成。 - 按需 ISR:通过 API 路由手动触发页面重新验证,避免定时轮询。
- 增量构建:Next.js 14+ 支持增量构建,只重新构建变更的页面。
TSX// 按需 ISR 示例(API 路由) // pages/api/revalidate.js export default async function handler(req, res) { if (req.query.secret !== process.env.REVALIDATION_SECRET) { return res.status(401).json({ message: 'Invalid token' }); } try { await res.revalidate('/posts/1'); return res.json({ revalidated: true }); } catch (err) { return res.status(500).json({ message: 'Error revalidating' }); } }
2. 数据一致性与安全性
- 不要在
getStaticProps中暴露敏感数据:如 API 密钥、数据库凭证。应使用环境变量(process.env.API_KEY)。 - 外部 API 调用必须有错误处理和超时机制:避免构建时因 API 故障导致整个构建失败。
TSXexport async function getStaticProps() { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 秒超时 const res = await fetch('https://jsonplaceholder.typicode.com/posts', { signal: controller.signal, }); clearTimeout(timeoutId); if (!res.ok) { throw new Error(`API responded with ${res.status}`); } const posts = await res.json(); return { props: { posts } }; } catch (error) { console.error('Failed to fetch posts:', error); return { props: { posts: [] } }; // 优雅降级 } }
3. 动态路由的路径生成
对于大量动态路由(如数百万个产品页面),构建时预渲染所有路径可能不现实。使用 fallback: 'blocking' 让首次请求时生成页面,后续请求直接返回缓存。
JSXexport async function getStaticPaths() { // 只预渲染热门产品的前 100 个 const res = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=100'); const posts = await res.json(); const paths = posts.map((post) => ({ params: { id: String(post.id) }, })); return { paths, fallback: 'blocking' }; }
4. CDN 缓存策略
使用 CDN 时,注意缓存策略。对于 ISR 页面,设置 Cache-Control: s-maxage=60, stale-while-revalidate 可让 CDN 缓存 60 秒,并在后台重新验证。
常见报错与排查
| 错误信息 | 原因 | 解决方案 |
|---|---|---|
Error: getStaticPaths is required for dynamic SSG pages | 动态路由页面未导出 getStaticPaths | 在 pages/posts/[id].js 中导出 getStaticPaths 并返回有效路径数组 |
Error: 'fetch' is not defined in getStaticProps | Node.js 版本低于 18,不支持原生 fetch | 升级 Node.js 到 18+,或安装 node-fetch / axios |
Error: A required parameter (slug) was not provided as a string in getStaticPaths | params 中的参数值不是字符串类型 | 确保使用 String() 转换,如 params: { slug: String(item.slug) } |
Error: The 'fallback' key must be a boolean or 'blocking' | fallback 字段值非法 | 检查 fallback 值,必须是 false、true 或 'blocking' 之一 |
常见问题 FAQ
Q: 如何在 Next.js SSG 中处理用户认证和个性化内容?
A: SSG 生成的页面是静态的,无法在构建时包含用户特定数据。对于需要认证的页面,建议使用客户端数据获取(如 useEffect + fetch)或服务端渲染(SSR)。也可以结合 NextAuth.js 等库,在客户端获取用户会话并动态渲染内容。
Q: 使用 SSG 时,如何更新已部署的静态页面?
A: 有三种方法:
- 全量重新部署:重新运行
next build并部署整个应用。 - 增量静态再生(ISR):在
getStaticProps中设置revalidate属性,让 Next.js 在后台重新生成页面。 - 按需 ISR:通过 API 路由手动触发页面重新验证,适合内容发布后立即更新。
Q: SSG 和 ISR 有什么区别?我应该如何选择?
A: SSG 在构建时生成所有页面,部署后内容不变。ISR 允许在运行时按需重新生成静态页面,结合了静态页面的性能和动态内容的灵活性。如果内容变化不频繁且可以接受构建时更新,选择 SSG。如果需要内容在部署后自动更新(如博客文章发布后立即生效),选择 ISR。
相关深度解决方案
在配置当前服务时,如果您需要实现更复杂的架构或多源数据整合,建议配合参考我们整理的 Nx 与 Turborepo 选型指南:大型团队与初创项目该如何抉择?。
在配置当前服务时,如果您需要实现更复杂的架构或多源数据整合,建议配合参考我们整理的 解决 HMR 不生效与状态丢失:Webpack 热模块替换实战排查。