1. SSG到底在解决什么问题,为什么2026年它又火了
如果说2024、2025年前端圈最热的关键词是React Server Components和边缘计算,那到了2026年,SSG(Static Site Generation,静态站点生成)反而重新回到了聚光灯下。很多准备前端面试的朋友看到“SSG”这个词,第一反应是“这不就是老技术吗,Jekyll时代的东西了”,但细看现在的大厂JD和面试题,SSG几乎每次都会被拎出来单独考察。它确实不新,但它解决的问题一点都没过时。
先聊清楚SSG到底是什么。静态站点生成,说的是在构建阶段就把页面内容全部渲染成纯静态HTML文件,不需要服务器在请求时现算,也不需要浏览器在运行时拼页面。用户在浏览器里输入网址,服务器直接吐出一个已经写好的HTML文档,浏览器解析完就能展示,不涉及JS框架的二次渲染、不等待接口返回、不做客户端路由映射。听起来很朴素,但这套思路在现在的Web环境下其实有特别强的现实价值。
我举个例子你就明白了。假设你要做一个帮助文档站点,内容大概是几百个Markdown文件,每个文件对应一个页面。如果用传统SPA(单页应用)方案,用户第一次进入会先加载一个空壳HTML,然后下载几百KB的JavaScript Bundle,浏览器再执行脚本、请求接口、渲染内容,整个过程用户要等两三秒才能看到正文。而SSG方案下,构建时就已经把这些Markdown文件全部转换成了静态HTML,用户访问哪个页面,服务器就返回哪个页面的完整HTML,首屏内容几乎瞬间到达。
这背后的逻辑是:并不是所有前端场景都依赖“动态”。内容文档、产品官网、博客、活动页面、营销落地页,这些场景的特点都是“写一次、读很多次”,内容更新频率低,但访问频率高,而且对首屏速度和SEO有明确要求。SSG就是为这类场景量身定制的方案。再说直白一点,SSG把“动态计算”的成本从用户访问时挪到了构建时,一次构建、到处执行,本质上是一种“预编译”的思路。
现在2026年前端面试题里为什么SSG相关问题越来越高频?因为面试官就是想考察候选人能不能把构建、渲染、数据流这些概念讲透,而不只是会写组件、调接口。理解了SSG,很多围绕性能优化、架构选型的面试问题都能迎刃而解。这篇内容就按我实际做项目的经验,把SSG的核心原理、工具选型、实操流程和踩坑点全部讲一遍,适合正在学习前端构建体系的人,也适合准备面试时想系统梳理SSG知识点的人。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. SSG、CSR、SSR三种方案到底怎么选
2.1 三种渲染方式的核心差异
很多同学对CSR、SSR、SSG的理解停留在“一个在浏览器渲染、一个在服务器渲染、一个在构建时渲染”,但真要问你“为什么这个项目用SSG而不用SSR”,就答不上来了。这里我拆开讲。
CSR(客户端渲染)的逻辑是:服务器只返回一个包含根节点的HTML和一堆Script标签,浏览器下载JS后执行,动态生成页面内容。好处是服务器压力小、前后端部署完全解耦、页面交互性强;坏处是首屏慢、SEO不友好,因为搜索引擎爬虫执行JS的能力有限,可能看到的就是一个空页面。
SSR(服务器端渲染)的逻辑是:每次用户请求页面时,服务器在Node.js环境里运行组件代码,拼接出完整HTML再返回给浏览器。好处是首屏快、SEO好;坏处是服务器承担了所有渲染压力,高并发场景下CPU会飙升,而且每次请求都要重复执行渲染逻辑,缓存策略做得不好时性能问题很严重。
SSG的逻辑是:构建时一次性把所有页面渲染成HTML,部署到任何静态服务器或CDN上就能对外服务。首屏速度和SEO天然达标,服务器压力几乎为零,也不用考虑Node.js运行时环境。坏处是“动态”能力最弱,如果页面内容实时性要求高(比如用户头像、购物车状态、实时数据),纯SSG搞不定。
三者的关系不是“哪个代替哪个”,而是看你的页面内容到底有多“动”。内容相对固定、更新频率低、访问量大,SSG就是最优解;需要实时数据、依赖用户状态,CSR或者SSR+客户端水合才更适合。
2.2 SSG的适用边界,什么项目别硬上
我自己给团队做技术选型时,判断一个项目适不适合SSG,就看三点:
第一,内容是否可以通过“构建时数据源”确定。这里的数据源可以是本地Markdown文件、CMS接口、远端API,只要在构建时能拿到数据,就能提前渲染成HTML。比如电商商品详情页,商品数据在CMS里,构建时拉取所有商品信息生成静态页,完全可行;但购物车页面、结算页依赖用户登录态,就不适合SSG。
第二,页面更新频率是否在可接受范围内。无论你用什么SSG框架,每次内容更新基本都要触发一次构建。内容更新很频繁的站点,比如每五分钟刷一次股价、每小时更新一次热点榜单,就不太适合纯SSG,这种场景更适合ISR(增量静态再生)或者直接上SSR。
第三,开发团队能不能接受“内容优先”的思维方式。SSG项目里,大多数页面是内容驱动的,开发时要先设计数据结构、内容模型,然后再考虑组件怎么渲染。习惯纯前端组件化开发的人可能有点不适应这种节奏。
这里插一句,面试时经常被问到“SSG是不是没落了”,其实不是。主流框架的ISR功能就是SSG的升级版——先按静态方式生成页面,过一段时间后后台异步重新构建增量页面,用户始终拿到的都是静态HTML,但内容保鲜程度大幅提升。所以很多实际项目用的不是“纯SSG”,而是“SSG+ISR”的组合方案。
3. 主流SSG工具怎么选,各家的设计哲学
3.1 三大梯队框架横向对比
市面上SSG工具非常多,我按适用场景分成了三梯队。
第一梯队是React生态的Next.js和Gatsby。Next.js是目前综合能力最均衡的框架,它支持SSG、SSR、ISR三种模式并存,一个项目里有的页面静态生成、有的服务端渲染、有的纯客户端渲染,灵活度非常高。Gatsby则是老牌React SSG框架,插件生态丰富,GraphQL数据层是它的特色,但现在团队维护活跃度明显不如Next.js。
第二梯队是Vue生态的Nuxt和Astro。Nuxt在Vue 3之后全面拥抱了SSG和SSR双模式,对Vue开发者来说上手成本最低。Astro则是2024年之后我个人非常看好的工具,它的核心理念是“默认零JavaScript”,页面组件默认只输出静态HTML,只有显式标记的组件才会带上JS,输出体积控制得极其激进。
第三梯队是内容站方向的Eleventy和Hugo。Eleventy(11ty)是一个极简主义的JavaScript静态站点生成器,没有框架运行时,模板支持多语言,特别适合做文档站。Hugo是Go语言写的,构建速度极快,几千个内容页也能在几秒内构建完,适合博客、官网、文档这类纯内容项目。
对比一下各工具的输出特点:
| 框架 | 语言生态 | 学习曲线 | 适合场景 | 动态能力 |
|---|---|---|---|---|
| Next.js | React | 中 | 混合渲染项目、大型站点 | 强(SSG/SSR/ISR) |
| Gatsby | React | 中高 | 内容型站点、博客 | 中(GraphQL数据层) |
| Nuxt | Vue | 中 | Vue技术栈项目 | 强(SSG/SSR双模式) |
| Astro | 多框架兼容 | 低 | 内容站、组件局部交互 | 中(岛屿架构) |
| Eleventy | JavaScript | 低 | 文档站、轻量博客 | 弱(纯静态) |
| Hugo | Go模板 | 低 | 极大型内容站 | 弱(纯静态) |
3.2 选型时容易被忽略的现实因素
技术选型不能只看框架特性,有几个现实问题很多人都栽过跟头。
一个是团队技术栈延续性。如果团队已经用React写了大半年的业务代码,硬切换到Astro虽然技术上可行,但团队的学习成本、组件复用成本都会增加。我见过一个项目非要引入Gatsby做官网,结果官网里要嵌一个小型在线工具模块,Gatsby的GraphQL数据层加上客户端动态路由,折腾了好几周,最后还不如直接用Next.js混合渲染来得快。
另一个是社区的活跃度和生态成熟度。2026年了,选SSG工具一定要看这个框架的周边生态是否足够活跃。Next.js的App Router虽然API一直在变,但胜在文档全、社区教程多、踩坑经验密;Eleventy和Hugo很稳定,但遇到冷门问题可能要在GitHub Issues里翻老半天。面试和实际项目里,选一个生态好的框架,后期维护成本差异非常大。
第三个是构建速度和增量更新的能力。内容站点做到后期内容上千篇之后,每次构建都要全量重新渲染,压力不小。Astro和Next.js的增量构建能力强,Hugo更是以速度著称。如果项目内容会持续增长,我建议你在选型时就把“构建耗时”作为一个明确指标去测一下,别等项目上线了才发现一次构建要跑十分钟。
4. 从零搭建一个SSG项目实操全流程
4.1 环境准备与项目初始化
接下来按我实际搭建项目的步骤走一遍,用Next.js来做演示,因为它的SSG能力和混合渲染能力都强,能覆盖大多数场景。
前置环境要装好Node.js 18以上版本,建议直接用当前LTS版本,npm或者pnpm都行。我习惯用pnpm,包安装速度快,磁盘占用也小。直接执行初始化命令:
bash复制npx create-next-app@latest ssg-demo
创建过程中会问几个配置选项,TypeScript建议选Yes,ESLint选Yes,App Router选Yes,因为这是Next.js现在主推的目录结构。TCPort默认3000即可。Tailwind CSS按需选,项目中要写样式就选,不选也没关系。
初始化完成后的目录结构大概长这样:
text复制ssg-demo/
├── app/
│ ├── layout.tsx
│ ├── page.tsx
│ └── globals.css
├── public/
├── next.config.js
└── package.json
App Router模式下,app目录里的每个文件夹对应一个路由。如果你用过Pages Router(老的pages目录模式)也问题不大,两种模式的核心思想一致,但App Router的写法更接近“文件即路由”的直觉。
4.2 配置SSG,关键就三步
Next.js里配置SSG有三个关键步骤。第一步,在页面组件里导出generateStaticParams函数,告诉框架“哪几个路径参数需要提前生成”。第二步,用fetch或者直接读取文件系统来获取数据,把获取到的数据作为静态内容渲染进HTML里。第三步,在页面顶部设置dynamicParams为false,明确告诉框架“只生成声明过的页面,其他路径返回404”。
举一个实际的文档站例子。假设你的文档内容放在content目录下,每个文档是独立的Markdown文件,文件名就是路由参数。在app/docs/[slug]/page.tsx里这样写:
typescript复制// app/docs/[slug]/page.tsx
import fs from 'fs/promises'
import path from 'path'
import matter from 'gray-matter'
import { remark } from 'remark'
import html from 'remark-html'
export const dynamicParams = false
export async function generateStaticParams() {
const files = await fs.readdir(path.join(process.cwd(), 'content'))
return files.map((file) => ({
slug: file.replace(/\.md$/, '')
}))
}
export async function generateMetadata({ params }: { params: { slug: string } }) {
const content = await fs.readFile(
path.join(process.cwd(), 'content', `${params.slug}.md`),
'utf-8'
)
const { data } = matter(content)
return {
title: data.title,
description: data.description
}
}
export default async function DocPage({ params }: { params: { slug: string } }) {
const content = await fs.readFile(
path.join(process.cwd(), 'content', `${params.slug}.md`),
'utf-8'
)
const { data, content: body } = matter(content)
const processed = await remark().use(html).process(body)
const contentHtml = processed.toString()
return (
<article>
<h1>{data.title}</h1>
<p>{data.description}</p>
<div dangerouslySetInnerHTML={{ __html: contentHtml }} />
</article>
)
}
这段代码里,generateStaticParams在构建时扫描content目录里的所有Markdown文件,生成所有文档页面的路径;页面组件读取对应文件,用gray-matter解析Frontmatter元数据,用remark加remark-html把Markdown内容转换成HTML字符串。整个构建过程中没有任何运行时逻辑,每个文档页面都是纯静态输出。
4.3 数据获取和内容管线的搭建思路
上面示例里用的是本地文件读取,但SSG的数据源远不止这一种。实际项目中常见的数据来源有三种:本地内容文件、CMS接口、远端Git仓库。
本地内容文件是最朴素、最可控的方式。把文章、文档以Markdown或MDX格式放在仓库里,既可以做版本管理,又能在构建时直接读取。缺点是需要开发者在Git仓库里维护内容,非技术人员操作成本高。
CMS接口是内容型站点的常用方案。构建时从CMS拉取接口数据,生成静态页面。比如用Strapi、Contentful等Headless CMS,后台编辑内容,前端构建时同步拉取。这种模式下,内容更新的流程是:运营在CMS编辑完内容,触发Webhook通知前端项目重新构建,新内容就被编译进了新的HTML文件里。Next.js的fetch接口默认支持缓存数据,配合cache配置可以控制何时重新拉取数据,但SSG场景下通常就是“构建时拉取一次”。
第三类是直接从Git仓库拉取内容,适合团队已经用Git管理文档的情况。可以写一个构建脚本,在构建前先git pull最新代码,再执行SSG生成。这个方案在某些内容源频繁变动的场景下很实用,缺点是比本地文件方案慢,需要额外等待拉取时间。
4.4 构建优化与输出产物解析
配置好页面和数据源之后,就要关注构建优化了。在Next.js项目里,SSG的构建输出会明确标识哪些页面是静态生成的。执行build命令的输出大概长这样:
text复制Route (app) Size First Load JS
┌ ○ / 1.5 kB 81.2 kB
├ ○ /docs/[slug] 248 B 78.6 kB
├ ● /docs/[slug] 322 B 79.1 kB
└ ○ /about 1.2 kB 80.3 kB
标了○符号的是静态生成的纯静态页面,标了●符号的表示该页面在构建时被预渲染为静态HTML。看到这个输出,就可以确认SSG配置成功了。
构建优化方面,我有几个实际建议。第一,给图片配置next/image组件,SSG生成的页面会自动处理图片的尺寸和格式转换,输出WebP格式,体积大幅缩减。第二,善用generateMetadata,SSG页面可以在构建时就生成完整的meta信息,SEO能力拉满。第三,CSS尽量全局引入,避免每个页面重复加载相同的样式文件,Next.js会默认做代码分割,但页面间共享的CSS最好提到根布局里。
最后是静态导出的选项。如果项目完全不需要任何Node.js服务端能力,可以使用next export命令,把整个站点导出成纯静态文件,部署到任意静态服务器上。但注意,使用next export时无法使用ISR、运行时重写等功能,导出物是一堆HTML、CSS、JS、图片文件的组合,非常适合部署到CDN或对象存储上。
5. 部署时的关键细节,尤其是Nginx配置
5.1 构建产物与部署目标
SSG项目的部署比传统SPA要简单得多,因为你只需要一个能返回静态文件的HTTP服务器。但“简单”不等于没有坑,实际部署时我遇到过不少问题。
先看构建产物。Next.js的SSG项目直接执行next build,生成.next目录;如果用了next export(在Next.js 14及以上版本要配置output: export),生成out目录,整个out目录里的文件就是完整的静态站点。把这些文件放到Nginx、Apache、CDN或者对象存储上都行。
部署目标和路径策略相关。如果你的站点部署在域名根路径下(比如https://example.com),那路径配置最简单。如果部署在子路径下(比如https://example.com/blog/),就要在next.config.js里配置basePath,构建时所有资源路径都会加上子路径前缀,否则静态资源的URL会指向错误的位置。
5.2 Nginx托管SSG站点的最佳实践
Nginx托管SSG项目的配置非常经典。一个常见的配置模板长这样:
nginx复制server {
listen 80;
server_name example.com;
root /var/www/ssg-demo/out;
index index.html;
location / {
try_files $uri $uri.html $uri/ =404;
}
location ~* \.(js|css|png|jpg|jpeg|gif|webp|svg|woff|woff2)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
location = /index.html {
add_header Cache-Control "no-cache";
}
}
这段配置有几个关键点。第一,root指向out目录,这是构建产物目录。第二,try_files指令处理路由匹配,$uri先尝试精确文件,$uri.html用于生成目录下扩展名被省略的HTML文件,$uri/处理目录路径,找不到就返回404。第三,静态资源设置了30天的强缓存,因为构建出的静态文件都带指纹(文件名里包含哈希值),内容变化后文件名会变,浏览器会重新请求,可以放心缓存。第四,index.html设置了no-cache,防止首页更新后浏览器继续用旧缓存。
试过你就知道,这个配置在静态页面场景下比SPA的Nginx配置简单太多了。SPA场景还需要配置try_files回退到index.html让前端路由接管,SSG场景每个路由都是真实存在的HTML文件,不存在“刷新404”的问题。
5.3 内容更新和增量构建策略
SSG项目上线后的内容更新,绕不开一个核心问题:内容改了,怎么让线上页面也更新?
最简单的方案是手动触发构建。内容在Git仓库里,改完代码之后在CI/CD平台(GitHub Actions、GitLab CI等)上重新跑一次build和deploy,构建出的新文件覆盖到服务器或CDN上,整个过程可能耗时一两分钟,但对内容更新频率不高的场景完全够用。
内容更新频率略高一点的场景,可以配置Build Hooks。比如Vercel平台提供Deploy Hooks功能,CMS保存内容时触发Webhook,Vercel自动重新构建站点;在自建CI体系里,也可以给Pinia、Jenkins或者GitLab CI配置Webhook触发器,思路是一样的。
还有一种方案不用全量构建。Next.js的ISR功能可以在运行时增量更新页面,配置了revalidate参数的页面,在用户访问的间隔时间超过指定秒数后,会在后台重新生成该页面的HTML并替换旧文件。但ISR本质上依赖Node.js服务器运行时的请求触发,所以它和纯静态导出是互斥的。如果你要的是“目标静态服务器,但内容更新希望能自动刷新”,优先考虑Webhook触发构建这条路线,不要折腾ISR。
6. 常见问题与排查技巧实录
6.1 构建成功了,但页面打开是空白
这个问题在SSG项目里出现过很多次,大多数原因是客户端水合(Hydration)失败。SSG生成的HTML里已经包含了完整的内容,但框架同时注入了事件绑定和客户端状态管理的JavaScript。如果页面使用的某些浏览器API(比如window、document)在构建时被模块顶层代码直接调用了,构建阶段不会报错,但浏览器端运行时会抛异常,让水合过程中断,最终表现为页面刚打开能看到内容闪一下,随后变成空白。
排查思路是先打开浏览器开发者工具的Console面板,看有没有报错信息。最常见的是“Hydration failed because the initial UI does not match what was rendered on the server”。出现这个提示,就说明服务端生成的HTML和客户端首次渲染的DOM不一致了。常见原因有:组件里用了Math.random()、Date.now()这类非确定性方法;某个组件依赖了用户代理字符串;CSS-in-JS的类名生成规则在两端不一致。
解决办法是,构建时和执行环境来确定的内容判断逻辑不做SSG处理。举例来说,用next/dynamic动态导入组件,并把ssr设置为false,该组件就不会参与服务端渲染,只在浏览器端渲染。
typescript复制const ClientOnlyComponent = dynamic(
() => import('./ClientOnlyComponent'),
{ ssr: false }
)
6.2 动态路由页面404
使用generateStaticParams构建动态路由时,忘了设置dynamicParams为false的话,访问未被声明的路径时,框架会尝试在服务器端动态生成页面,而不是直接返回404。这本身不是问题,但如果你使用了纯静态导出模式,没有服务器端运行时,这种行为会导致请求一直找不到对应资源。
更常见的情况是,参数命名不一致。假设你的内容文件名是blog-post-01.md,你在generateStaticParams里返回的参数名是slug,页面文件目录是app/blog/[slug]/page.tsx,这时文件名会被正确匹配;但如果你的页面文件是app/blog/[id]/page.tsx,generateStaticParams里返回的却是param,框架就找不到对应页面了。参数名必须和目录里的方括号名完全一致,一个字母都不能差。
排查这个问题的快速方法是,执行build之后在终端输出里查看“Route”部分,确认动态路由是否被正确生成。如果看到[docs]这类带方括号的路径被标记为静态生成,那就说明路由匹配成功;如果在该路径下面是空白的,就要检查文件名和参数名的对应关系了。
6.3 部署到Nginx后样式全丢了
这个坑我踩过不止一次。本地构建完预览正常,部署到Nginx后页面打开只有纯文本,所有CSS和图片都加载失败。打开开发者工具的Network面板,会发现js、css请求都返回404。
根本原因还是资源路径的问题。Next.js默认认为资源部署在域名根路径下,所以构建出的CSS、JS的引用路径是/_next/static/...。如果你的站点配置了子路径(比如部署在https://example.com/blog/下),这些路径就会请求到https://example.com/_next/static/...,而实际文件在服务器上位于/blog/_next/static/...,自然404了。
解决办法很简单,在next.config.js里设置basePath,让框架知道资源路径要加上子路径前缀:
javascript复制// next.config.js
module.exports = {
basePath: '/blog',
output: 'export'
}
设置完重新构建,产物里所有资源路径都会自动带上/blog前缀。这里还要提醒一句,如果basePath变了,Nginx配置里的root路径也要确保对应到新构建产物目录,别让新旧文件混在一起,否则容易出一些很难排查的缓存问题。
6.4 我的个人心得:SSG项目的三个习惯
做了几个SSG项目之后,我总结出三个不错的习惯。
第一,把内容、组件、配置分离。内容放content目录、组件放components目录、站点配置(导航、标题、社交链接)放site.config.ts,理由很简单,内容运营和前端开发的职责边界清晰了,出问题时能快速定位是内容问题还是代码问题。
第二,构建脚本里加入内容源的校验逻辑。比如内容文件里的Frontmatter缺少title或者date字段时,构建直接报错。这样能在构建阶段就发现问题,而不是发布上线后才发现页面标题是空的。用TypeScript写内容处理脚本的话,可以直接定义Frontmatter的类型,让类型系统强制校验。
第三,善用CI流程做预览。在GitLab或GitHub Actions里,每个PR都触发一次SSG构建,并把构建产物部署到临时预览环境,联调时可以提前看到效果。这个习惯能大大减少正式环境出错的概率。
SSG这套方案,我现在做内容型项目时基本是首选。它的核心思路——提前构建、静态输出——看起来很朴素,但恰恰是这种“笨功夫”换来了极致的速度和稳定性。实际开发过程中,把数据的获取时机、页面的生成方式、资源的发布策略都想清楚,一个中等规模的内容站,从搭建到上线,熟练的话两三天就能跑通。希望这篇内容对你有帮助,尤其是准备前端面试的朋友,把SSG的原理和实操流程吃透,再问到构建性能相关的问题,你就能从原理说到实践,给对方一个完整的答案。
