Static Generation实战:提升网站性能与SEO的静态生成方案
优化前:卡得不行
上个月接手一个老项目,用的是 Next.js + Static Generation(SSG),理论上应该飞快才对。结果一打开首页,Lighthouse 直接给我打了个 42 分,FCP(First Contentful Paint)5.2 秒,TTI(Time to Interactive)更是飙到 6.8 秒。用户反馈“点进去以为页面挂了”,我自己本地 dev 模式跑都卡得想砸键盘。
最离谱的是,明明是静态生成的页面,为什么还这么慢?数据量也不大,就几百个产品页,每个页面也就拉个 JSON 文件渲染。我一开始以为是图片没优化,结果把所有图片换成占位图,加载时间只降了 300ms —— 根本问题不在那。
找到瓶颈了!
折腾了半天,打开 Chrome DevTools 的 Performance 面板录了个加载过程,发现主线程在 getStaticProps 里疯狂跑 JS,而且每个页面都重复拉同一个 API 接口。原来问题出在这:我们用 getStaticPaths 生成了 500 多个动态路由,每个路由的 getStaticProps 都独立调用一次远程 API 获取数据。
比如:
// pages/products/[id].js
export async function getStaticProps({ params }) {
const res = await fetch(https://jztheme.com/api/products/${params.id});
const product = await res.json();
return { props: { product } };
}
这代码看着没问题,但构建时 500 个页面就发 500 次请求,不仅慢,还把测试 API 打崩了两次(别问,问就是被运维骂了)。更糟的是,这些产品数据其实变动极低,完全没必要每个页面单独拉。
再看 Lighthouse 报告,最大的问题是“减少主线程工作”和“避免重复网络请求”。好,方向明确了:合并数据拉取,减少 I/O 次数。
方案一:全量预取 + 内存缓存(失败)
我第一反应是:干脆在构建开始时一次性拉完整个产品列表,然后在 getStaticProps 里直接从内存读。试了下:
// lib/data.js
let allProducts = null;
export async function fetchAllProducts() {
if (allProducts) return allProducts; // 缓存
const res = await fetch('https://jztheme.com/api/products');
allProducts = await res.json();
return allProducts;
}
然后在每个页面里:
import { fetchAllProducts } from '@/lib/data';
export async function getStaticProps({ params }) {
const products = await fetchAllProducts();
const product = products.find(p => p.id === params.id);
return { props: { product } };
}
想法很美,但实际构建时 Next.js 的 SSG 是并行执行 getStaticProps 的,每个 worker 进程都有自己独立的内存空间,allProducts 缓存根本没生效。结果还是发了 500 次请求,只是这次是并发打过去,API 更扛不住了。这方案直接废掉。
方案二:构建时写本地 JSON(亲测有效)
既然内存缓存靠不住,那就落地到磁盘。我在根目录加了个 scripts/build-data.js,在构建前先跑这个脚本:
// scripts/build-data.js
import fs from 'fs';
import path from 'path';
async function main() {
const res = await fetch('https://jztheme.com/api/products');
const products = await res.json();
// 按 ID 分文件存,方便后续按需读
const dataDir = path.join(process.cwd(), 'public/data');
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
products.forEach(product => {
fs.writeFileSync(
path.join(dataDir, ${product.id}.json),
JSON.stringify(product)
);
});
console.log(✅ Built ${products.length} product data files);
}
main();
然后在 package.json 里改构建命令:
{
"scripts": {
"build": "node scripts/build-data.js && next build"
}
}
页面里就简单多了,直接读本地文件:
// pages/products/[id].js
import fs from 'fs';
import path from 'path';
export async function getStaticProps({ params }) {
const filePath = path.join(process.cwd(), 'public/data', ${params.id}.json);
const product = JSON.parse(fs.readFileSync(filePath, 'utf8'));
return { props: { product } };
}
这里注意我踩过好几次坑:一定要用 process.cwd(),别用相对路径,否则在 CI/CD 环境容易找不到文件。另外,public/data 目录会被 Next.js 当作静态资源,但因为我们只在构建时读,运行时不会暴露,所以安全。
改完后重新构建,时间从原来的 3 分 12 秒降到 48 秒。更重要的是,API 只被调了一次,再也不怕被打崩了。
性能数据对比
部署上线后,用 WebPageTest 测了三次取平均值:
- 优化前:FCP 5.2s,TTI 6.8s,Lighthouse 性能分 42
- 优化后:FCP 780ms,TTI 920ms,Lighthouse 性能分 96
首屏加载快了将近 7 倍,用户反馈“秒开”。而且因为数据是构建时生成的,CDN 缓存效率极高,连带降低了服务器压力。
不过有个小问题:如果产品数据更新,必须重新触发构建。但我们业务场景是每天凌晨同步一次数据,所以完全可接受。如果你的数据是实时变动的,可能得考虑 ISR(Incremental Static Regeneration),但那是另一个故事了。
其他小优化(顺手做了)
除了主干优化,我还顺手干了几件事:
- 把
getStaticPaths的fallback: true改成false,因为所有路径都是已知的,没必要让未知路径走 fallback - 给 JSON 文件加了 gzip 压缩(Next.js 默认支持,不用动)
- 移除了页面里没用的第三方库,减小了 bundle 体积
这些改动加起来又省了 100-200ms,但比起主干优化,算是锦上添花了。
结尾
以上是我这次 Static Generation 性能优化的实战经验。核心思路就一点:别在 SSG 里做重复、低效的 I/O。能合并就合并,能预生成就预生成。虽然写本地 JSON 看着有点“土”,但胜在简单、稳定、效果拔群。
这个方案不是最优解(比如用数据库缓存可能更优雅),但对我们这种小团队来说,维护成本最低,出问题也最容易排查。改完后我终于能睡个好觉了,不用半夜被报警叫醒说 API 被打挂了。
以上是我踩坑后的总结,希望对你有帮助。有更优的实现方式欢迎评论区交流,比如你们怎么处理大规模 SSG 数据拉取的?

暂无评论