嘿,大家好!欢迎来到这场关于“让 React 像闪电一样快”的讲座。我是你们的向导,一个在代码堆里摸爬滚打多年的资深老司机。
今天我们不聊那些虚头巴脑的 React Hooks 基础,也不搞那些花里胡哨的动画库。今天,我们要聊聊一个能让你的网页在 0.1 秒内从服务器跳到用户屏幕上的黑科技——静态站点生成。
想象一下,你走进一家面包店。你是想看厨师在后面现揉面、现发酵、现烘烤(这叫服务端渲染,SSR,虽然也快,但每单都要烤),还是想直接拿一盒已经烤好、包装精美的面包(这叫静态站点生成,SSG,打开就能吃)?
SSG 就是那个“拿现成面包”的策略。它在构建期就把你的 React 页面预渲染成了 HTML 文件。用户访问时,服务器直接把 HTML 发过去,React 只负责在浏览器里做一点点“交互”,就像给面包加点果酱一样。这速度,简直比光速还快(好吧,比光速慢一点点,但在网络延迟面前,这叫神速)。
好了,废话不多说,让我们直接把键盘敲得噼里啪啦响,看看怎么把 React 变成 SSG。
第一部分:从 CSR 到 SSG 的“降维打击”
在 React 的世界里,我们以前最常用的模式是 CSR(客户端渲染)。这就像是你去餐馆,服务员端上来一张白纸,告诉你:“先生,菜单在您的手机 App 里,请点击这里,再点击那里,再等两秒钟,我们后厨才能开始做菜。”
用户体验好吗?不好。SEO(搜索引擎优化)好吗?不好。因为搜索引擎爬虫看不到你的菜,它只看到一张白纸。
SSG 的核心逻辑:
在构建期,Node.js 环境运行你的代码,把组件渲染成 HTML 字符串,把这些字符串写入硬盘的 build 文件夹里。当用户访问时,你直接送给他 HTML。
代码示例 1:最简单的 SSG 示范
别急,先看一个最基础的组件。注意看 export const getStaticProps 这个函数,这是 SSG 的魔法入口。
// pages/index.js
// 1. 这是一个纯组件,和以前写法一样
const HomePage = ({ message, timestamp }) => {
// 这里的 window 对象是安全的,因为这是在浏览器里跑
return (
<div style={{ padding: '20px', fontFamily: 'Arial' }}>
<h1>SSG 极速加载演示</h1>
<p>这是一个静态生成的页面。</p>
<p>当前时间: {timestamp}</p>
<p>消息: {message}</p>
<button onClick={() => alert('我是静态页面,但我也能动!')}>
点击我
</button>
</div>
);
};
// 2. 这里的 getStaticProps 会在构建时运行
// 就像是一个预演,告诉 React:“嘿,兄弟,把这些数据算好,生成 HTML 吧。”
export const getStaticProps = async () => {
// 模拟从 API 获取数据
const data = {
message: "欢迎来到静态世界的尽头!",
timestamp: new Date().toLocaleString(),
};
// 返回 props,这些 props 会自动传给组件
return {
props: data,
// 可选:重新验证的时间(稍后讲 ISR)
revalidate: 10,
};
};
export default HomePage;
看懂了吗?getStaticProps 在构建时执行。服务器把组件渲染成了 HTML。当你把这个 HTML 部署到 CDN 上时,用户打开网页,根本不需要等待 JavaScript 下载和执行,页面瞬间就出来了!
第二部分:动态路由与 getStaticPaths —— 如何处理“成千上万”个页面
如果只有一两个页面,SSG 简直是神技。但现实是残酷的,你的博客有成千上万篇文章,你的电商网站有成千上万种产品。你不可能一个个手写页面文件。
这时候,React Router 的动态路由 [id].js 就派上用场了。但怎么让 SSG 知道要生成哪些页面呢?这就需要 getStaticPaths。
代码示例 2:动态博客列表
假设你有一个博客系统。
// pages/posts/[id].js
// 这是一个通用的组件,用来显示具体的文章
const PostPage = ({ post }) => {
return (
<div>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</div>
);
};
// 关键来了:getStaticPaths
// 它告诉 Next.js:“构建时,请先生成以下这些路径对应的 HTML 文件。”
export const getStaticPaths = async () => {
// 模拟从 CMS 或数据库获取所有文章的 ID
const paths = [
{ params: { id: 'react-is-awesome' } },
{ params: { id: 'ssg-is-fast' } },
{ params: { id: 'nextjs-rules' } }
];
// fallback: false 意味着如果访问了路径里没有的 id,会返回 404 页面
// fallback: 'blocking' 意味着如果路径没有预生成,Next.js 会临时生成一个,然后缓存起来
return {
paths,
fallback: false,
};
};
// getStaticProps 在这里运行,专门处理对应路径的数据
export const getStaticProps = async ({ params }) => {
const res = await fetch(`https://api.example.com/posts/${params.id}`);
const post = await res.json();
return {
props: {
post,
},
};
};
export default PostPage;
深度解析 getStaticPaths:
这里有个坑,新手最容易踩。getStaticPaths 返回的 paths 数组,决定了预渲染的页面数量。
- 场景 A: 你有 10,000 篇文章。你把所有 ID 都写进
paths数组?内存会爆炸,构建会超时,你的电脑会冒烟。 - 场景 B: 你只写前 100 篇?用户点第 101 篇怎么办?404。
解决方案:ISR(增量静态再生)。这是 SSG 的进化版,我们稍后再详细唠叨。但核心思想是:fallback: 'blocking'。这意味着,当用户访问一个未预生成的页面时,Next.js 会像 SSR 一样现场生成一次,然后缓存起来,下次再访问就快了。
第三部分:ISR —— SSG 的补丁与进化
SSG 有个硬伤:数据是死的。文章更新了,静态页面还是旧的,直到你重新部署。
为了解决这个问题,我们引入了 ISR(Incremental Static Regeneration,增量静态再生)。
ISR 允许你设置一个 revalidate 时间。比如你设置 revalidate: 60。这意味着:
- 用户访问页面,拿到的是最新的静态 HTML。
- 当这个页面在浏览器中处于活跃状态超过 60 秒后(或者是用户访问后 60 秒),Next.js 后台会悄悄发起一个请求去更新这个页面的数据。
- 更新成功后,下次用户访问时,就能看到新内容了。
代码示例 3:ISR 实战
// pages/news/[slug].js
export const getStaticProps = async ({ params }) => {
const news = await fetch(`https://api.example.com/news/${params.slug}`).then(res => res.json());
return {
props: { news },
// 核心魔法:这个页面每 60 秒自动更新一次
revalidate: 60,
};
};
export const getStaticPaths = async () => {
// 假设我们只预生成热门新闻,其他新闻由 ISR 处理
const popularNews = await fetch('https://api.example.com/news/popular').then(res => res.json());
const paths = popularNews.map(news => ({ params: { slug: news.slug } }));
return {
paths,
// 关键配置:允许未预生成的路径通过 ISR 现场生成
fallback: 'blocking',
};
};
const NewsPage = ({ news }) => {
return <div>{news.title} - 发布时间: {new Date().toLocaleTimeString()}</div>;
};
export default NewsPage;
为什么 fallback: 'blocking' 这么重要?
它解决了 SSG 的“冷启动”问题。如果网站有 100 万个产品页面,你不可能全部预渲染。
fallback: false:你只能访问预渲染的页面,其他全是 404。这等于没做 SSG。fallback: true:访问未预渲染页面时,会立即返回一个“加载中”的 HTML(通常是空页面或骨架屏),然后 Next.js 后台生成,生成完后再把 HTML 返回给用户。fallback: 'blocking':访问未预渲染页面时,Next.js 会阻塞用户的请求,直到页面生成完毕。这保证了用户体验,但首次访问未生成页面时会有轻微延迟(通常在 1-2 秒内,取决于数据源速度)。
第四部分:构建期的数据获取策略
SSG 的核心是“构建期”。这意味着你不能在 getStaticProps 里使用 window 或 document,因为构建时根本就没有浏览器环境!
代码示例 4:构建期 vs 运行期
// ❌ 错误示范
export const getStaticProps = async () => {
const data = await fetch('https://api.com/data');
// 危险!构建时 window 是 undefined!
const isBrowser = typeof window !== 'undefined';
return { props: { isBrowser } };
};
// ✅ 正确示范
export const getStaticProps = async () => {
// 纯粹的 Node.js 逻辑
const response = await fetch('https://api.com/data', {
headers: {
// 如果你的 API 需要 Authorization,这里可以设置
'Authorization': `Bearer ${process.env.API_SECRET}`,
},
});
const data = await response.json();
return {
props: { data },
};
};
// 组件里判断
const MyComponent = ({ data }) => {
// 这里 window 是可用的
const handleClick = () => {
console.log('Clicked', data.id);
};
return <button onClick={handleClick}>{data.title}</button>;
};
高级技巧:使用 fetch 的默认缓存策略
Next.js 13+ 对 fetch 做了巨大的优化。默认情况下,fetch 请求会被 Next.js 缓存。这意味着在构建期,getStaticProps 里的 fetch 请求可能会被缓存,导致你获取不到最新的数据(除非你刷新了构建)。
如果你想确保每次构建都获取最新数据,或者想在构建期使用 POST 请求,你需要设置 { cache: 'no-store' }。
export const getStaticProps = async () => {
const res = await fetch('https://api.example.com/data', {
cache: 'no-store' // 强制不缓存,每次构建都去拉取
});
// ...
};
第五部分:图片、字体与性能优化 —— 别让你的页面变成“重武器”
SSG 生成的 HTML 虽然快,但如果里面塞满了几兆的图片,用户打开还得下载 JS,那速度依然感人。
1. 图片优化:next/image
别再用 <img> 标签了,那是上个世纪的产物。用 <Image> 组件。它会自动压缩图片、提供响应式尺寸、懒加载。
import Image from 'next/image';
const HeroSection = () => {
return (
<div>
<h1>欢迎来到极速世界</h1>
{/* next/image 会自动处理这些事情,你只需要指定宽高 */}
<Image
src="/hero-image.jpg"
alt="Hero Image"
width={800}
height={600}
priority // 如果这是首屏图片,加上这个,防止闪烁
/>
</div>
);
};
注意: 使用 next/image 时,图片必须放在 public 文件夹下,或者必须是外部 URL(需要配置 images.domains)。
2. 字体优化:next/font
Google Fonts 加载慢,还会阻塞渲染。next/font 可以自动优化字体,甚至内联字体,让浏览器直接使用字体文件,无需额外的 HTTP 请求。
import { Inter } from 'next/font/google';
// 配置字体
const inter = Inter({ subsets: ['latin'] });
const App = () => {
return (
<main className={inter.className}>
{/* 现在字体已经内联,加载速度极快 */}
<h1>Hello, Next.js!</h1>
</main>
);
};
第六部分:自定义 Webpack/Vite —— 当 Next.js 太重时
虽然 Next.js 很强大,但有时候我们想保留对构建工具的完全控制权,或者不想引入 Next.js 那么庞大的依赖。这时候,我们可以手动配置 Webpack 或 Vite 来实现 SSG。
方案 A:Vite + React Router
Vite 是目前最流行的构建工具,它天生支持 SSR/SSG。
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { ssg } from 'vite-plugin-ssr'; // 关键插件
export default defineConfig({
plugins: [
react(),
ssg({ // 启用静态站点生成
include: '/**/*.html', // 指定哪些路由生成 HTML
exclude: ['/api/**'], // 排除 API 路由
}),
],
});
然后在你的入口文件(entry.ssr.js)中,手动渲染路由:
// entry.ssr.js
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import App from './App';
export default async function render(url, manifest) {
// 这里你可以解析 url,决定渲染哪个组件
// 模拟路由匹配
let Component = App;
const html = ReactDOMServer.renderToString(<Component url={url} />);
return {
html,
documentProps: {
title: 'My Static Site',
},
};
}
这种方法更轻量,适合那些只需要简单静态化,不需要 Next.js 那些高级功能(比如自动路由、API Routes)的项目。
方案 B:手动 Webpack 构建
如果你还在用 Webpack 5,你可以使用 html-webpack-plugin 在构建期生成 HTML 文件,然后配合 copy-webpack-plugin 把这些 HTML 和打包后的 JS 文件复制到 dist 目录。
但这需要你自己处理 React 的 hydration 过程,稍微有点繁琐。通常情况下,除非你有极其特殊的定制需求,否则直接用 Next.js 是最明智的选择。
第七部分:SSG 的架构陷阱与最佳实践
作为资深专家,我必须告诉你,SSG 并不是万能药,用不好就是“定时炸弹”。
1. 数据新鲜度与更新策略
SSG 的核心是“缓存”。如果你在 SSG 页面中展示实时数据(比如股票价格、在线人数),你会非常痛苦。
- 策略: 尽量将实时数据与静态数据分离。静态数据(文章、产品)用 SSG。实时数据(价格)用 API,在前端通过轮询或 WebSocket 获取。
- ISR 的运用: 对于新闻、博客这种内容,使用 ISR 是最佳实践。设置一个合理的
revalidate时间(比如 5-10 分钟),在数据更新频率和加载速度之间取得平衡。
2. window 和 document 的幽灵
正如前面提到的,在 getStaticProps 里千万别用 window。
但是,在组件内部,window 是存在的。这会导致一个常见的 Bug:你试图在 useEffect 里获取数据,但数据已经通过 getStaticProps 传进来了,你根本不需要再请求一次!
// ❌ 重复请求
const MyComponent = ({ data }) => {
useEffect(() => {
fetch('/api/similar-data').then(...) // 这是多余的!
}, []);
return <div>{data}</div>;
};
// ✅ 直接使用 props
const MyComponent = ({ data }) => {
// 直接渲染,不需要 useEffect
return <div>{data}</div>;
};
3. 预渲染的 HTML 结构
SSG 生成的 HTML 必须是完整的 DOM 结构。如果你在组件里使用了条件渲染(比如 if (loading) return null),在构建期,这个 loading 状态可能不会出现,导致生成的 HTML 是空的。
最佳实践: 在 getStaticProps 完成后,确保组件始终渲染出基本的骨架结构,而不是在构建期返回 null。
第八部分:大型项目的 SSG 策略
如果你的项目有几万个页面,全量预渲染(生成几万个 HTML 文件)会导致构建时间过长(可能长达 30 分钟甚至更久)。
这时候,我们需要一种混合策略。
- 首页、核心落地页、热门内容:使用 SSG,生成完整的 HTML。
- 长尾内容、搜索结果页、归档页:使用 SSR 或者 ISR (
fallback: true)。- SSR:每次请求都生成,保证数据最新。
- ISR:第一次请求生成,后续请求命中缓存。
// pages/search/[query].js
// 搜索页用 SSR,因为搜索结果经常变,而且数据量小
export async function getServerSideProps(context) {
const { query } = context;
const results = await fetch(`/api/search?q=${query}`).then(res => res.json());
return { props: { results } };
}
// 首页用 SSG
// pages/index.js
export async function getStaticProps() {
// ...
return { props: { featuredPosts } };
}
这种混合渲染模式是大型企业级应用的标准配置。它既保证了核心页面的极速加载(SEO 友好),又保证了动态内容的实时性。
第九部分:构建流程深度解析
当你运行 npm run build 时,到底发生了什么?让我们把 Next.js 的黑盒打开看看。
- 收集页面信息:Next.js 扫描
pages目录,识别所有带有getStaticProps的文件。 - 调用
getStaticProps:对于每个页面,Next.js 启动一个 Node.js 进程,运行你的代码,获取数据。 - 生成 HTML:拿到数据后,Next.js 调用 React 的
renderToString或renderToPipeableStream,生成 HTML 字符串。 - 写入文件:Next.js 将生成的 HTML 写入
.next/static/pages/目录。 - 生成路由:Next.js 在
.next/server/pages/目录下生成对应的路由处理文件(.js文件),这些文件负责在运行时处理getStaticPaths中定义的动态路由。 - 优化资源:压缩 CSS、JS,生成 manifest 文件。
构建时的错误处理:
如果在 getStaticProps 中抛出错误,Next.js 会跳过该页面的构建,并在控制台打印错误。生成的 HTML 文件将包含错误信息(或者你配置的 Fallback 组件)。注意: 这种错误不会导致整个构建失败,除非你配置了 onBuildError。
第十部分:实战案例——一个完整的电商 SSG 站点
让我们综合一下前面学到的所有知识,构建一个简单的电商产品页。
需求:
- 产品列表页(动态路由,使用
getStaticPaths)。 - 产品详情页(SSG,展示产品信息)。
- 使用
next/image优化图片。 - 使用 ISR 处理库存更新。
1. 路由配置
// pages/products/[slug].js
import Image from 'next/image';
import Link from 'next/link';
// 1. 定义哪些产品会被预渲染
export const getStaticPaths = async () => {
// 假设我们从 CMS 获取所有产品 ID
const products = await fetch('https://api.shop.com/products').then(res => res.json());
const paths = products.map(p => ({
params: { slug: p.slug }
}));
return {
paths,
fallback: 'blocking', // 未预渲染的产品允许 ISR 生成
};
};
// 2. 获取每个产品的数据
export const getStaticProps = async ({ params }) => {
const res = await fetch(`https://api.shop.com/products/${params.slug}`);
const product = await res.json();
return {
props: { product },
revalidate: 30, // 30秒后重新生成
};
};
// 3. 组件
const ProductPage = ({ product }) => {
return (
<div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
<Link href="/">← 返回列表</Link>
<h1>{product.name}</h1>
<div style={{ position: 'relative', width: '100%', height: '400px', marginBottom: '20px' }}>
<Image
src={product.image}
alt={product.name}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</div>
<p>价格: ${product.price}</p>
<p>库存: {product.stock}</p>
<button style={{ padding: '10px 20px', cursor: 'pointer' }}>
加入购物车
</button>
</div>
);
};
export default ProductPage;
2. 产品列表页
// pages/products/index.js
import Link from 'next/link';
export const getStaticProps = async () => {
const products = await fetch('https://api.shop.com/products').then(res => res.json());
return {
props: { products },
};
};
const ProductList = ({ products }) => {
return (
<div>
<h1>所有产品</h1>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: '20px' }}>
{products.map(product => (
<Link key={product.id} href={`/products/${product.slug}`} passHref>
<div style={{ border: '1px solid #ccc', padding: '10px', cursor: 'pointer' }}>
<div style={{ height: '150px', background: '#f0f0f0', marginBottom: '10px' }}>
{/* 这里可以用 next/image 优化,但为了演示简单省略 */}
<img src={product.image} alt={product.name} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
</div>
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
</Link>
))}
</div>
</div>
);
};
export default ProductList;
分析:
- 列表页是纯静态的(SSG),加载极快。
- 点击列表项跳转到详情页。
- 如果详情页已经预渲染,它瞬间加载。
- 如果详情页没预渲染(fallback: ‘blocking’),Next.js 现场生成,然后缓存。
- 库存信息每 30 秒自动更新(ISR)。
第十一部分:SSG 的未来与边缘计算
随着 Vercel、Cloudflare Workers 等边缘计算的发展,SSG 的概念正在发生演变。
传统的 SSG 是在构建时生成的,部署到全球 CDN。但如果数据需要实时更新,还是得请求服务器。
Edge SSG(边缘静态站点生成) 结合了 SSG 和 SSR 的优点。
- 在边缘节点(离用户更近的地方)进行构建/渲染。
- 使用边缘函数处理动态数据请求。
- 将结果缓存为静态文件。
这就像是把面包店开到了你家楼下的便利店,你买到的面包是现烤的(动态),但它是包装好的(静态),而且离你只有一步之遥。
虽然 Next.js 13+ 的 App Router 已经开始支持这种模式,但传统的 SSG 依然是目前构建高性能内容网站的基石。
第十二部分:总结与避坑指南(干货中的干货)
好了,老司机要交底了。在实际项目中,我见过太多人把 SSG 用得乱七八糟。这里列出几个最常见的坑:
-
不要在
getStaticProps里用localStorage- 构建时根本没有浏览器环境,你会得到
undefined。这会导致你的应用崩溃。
- 构建时根本没有浏览器环境,你会得到
-
不要过度预渲染
- 如果你有 100 万个页面,别试图全部预渲染。使用
fallback: true或blocking。构建时间过长是项目失败的直接原因。
- 如果你有 100 万个页面,别试图全部预渲染。使用
-
SEO 的坑:动态路由的 404
- 如果你用了
getStaticPaths但没有配置fallback,那么任何不在列表里的 URL 都会返回 404。搜索引擎可能会认为你的网站有死链,降低权重。务必配置fallback。
- 如果你用了
-
图片路径问题
- 在 SSG 环境下,
next/image的src必须是绝对路径或者是public下的相对路径。如果你从 API 拿到的是相对路径(比如/images/a.jpg),next/image可能无法正确处理,因为它会尝试去构建时请求这个相对路径,导致 404。解决方案通常是配置images.loader或者直接使用next/image的unoptimized属性(虽然不推荐)。
- 在 SSG 环境下,
-
TypeScript 的类型推断
- 现在的 Next.js 对 TypeScript 支持很好。
getStaticProps返回的props会自动推断为组件的props。如果类型不匹配,TypeScript 会直接报错,帮你避开 90% 的运行时错误。
- 现在的 Next.js 对 TypeScript 支持很好。
最后,记住这句话:
SSG 不是让你放弃动态性,而是用构建时的计算换取运行时的极速。它是 Web 性能优化的圣杯。
当你看到你的网站在 Lighthouse 测试中拿到 100 分,首屏加载时间在 0.5 秒以内,你会感谢今天坐在这里听讲座的你的。现在,去构建你的静态站点吧!