Перейти к содержимому

7. Статическая генерация (SSG)

Static Site Generation — это когда HTML генерируется один раз при сборке проекта и раздаётся всем пользователям как статический файл. Представь: ты испёк 1000 пирогов заранее и раздаёшь их без готовки — вместо того чтобы готовить каждый раз по заказу! 🥧

Обычный SSR: SSG:
────────────────── ────────────────────────────────
Запрос → Сервер При сборке (build time):
→ загружает данные → generateStaticParams()
→ рендерит HTML → fetch/db запросы
→ отправляет клиенту → рендеринг HTML
→ 200ms для каждого → сохранение .html файлов
Запрос → CDN
→ отдаёт .html файл
→ 5ms для каждого ⚡

В App Router компонент статичен по умолчанию, если не использует динамические API:

// app/about/page.tsx — автоматически SSG!
// Нет cookies(), headers(), searchParams → статическая страница
export default async function AboutPage() {
// Этот fetch выполнится ОДИН РАЗ при сборке
const team = await fetch('https://api.example.com/team', {
cache: 'force-cache', // или next: { revalidate: false }
}).then(r => r.json());
return (
<main>
<h1>О нас</h1>
<TeamList members={team} />
</main>
);
}
// → Генерирует /about.html при npm run build
// → Хранится на CDN, отдаётся мгновенно

📋 generateStaticParams: Динамические страницы → Статика

Заголовок раздела «📋 generateStaticParams: Динамические страницы → Статика»

Для динамических роутов нужно указать, какие params генерировать:

app/blog/[slug]/page.tsx
interface Props {
params: Promise<{ slug: string }>;
}
// Говорим Next.js: сгенерируй эти страницы при сборке
export async function generateStaticParams() {
// Запрашиваем все возможные slug при сборке
const posts = await fetch('https://api.blog.com/posts').then(r => r.json());
return posts.map((post: { slug: string }) => ({
slug: post.slug,
}));
// Например: [{ slug: 'hello-world' }, { slug: 'next-js-guide' }, ...]
}
export default async function BlogPost({ params }: Props) {
const { slug } = await params;
// Этот fetch тоже выполнится при сборке (для каждого slug)
const post = await fetch(`https://api.blog.com/posts/${slug}`, {
cache: 'force-cache',
}).then(r => r.json());
if (!post) notFound();
return (
<article>
<h1>{post.title}</h1>
<time>{post.publishedAt}</time>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}

Несколько параметров:

app/[lang]/blog/[slug]/page.tsx
export async function generateStaticParams() {
const languages = ['ru', 'en', 'de'];
const posts = await getAllPosts();
// Возвращаем все комбинации lang + slug
return languages.flatMap(lang =>
posts.map(post => ({
lang,
slug: post.slug,
}))
);
// [{ lang: 'ru', slug: 'hello' }, { lang: 'en', slug: 'hello' }, ...]
}

app/blog/[slug]/page.tsx
// По умолчанию: true — неизвестные slug генерируются динамически
// Установи false — 404 для неизвестных slug
export const dynamicParams = false; // или true (default)
export async function generateStaticParams() {
return [
{ slug: 'hello-world' },
{ slug: 'about-nextjs' },
];
}
export default async function BlogPost({ params }: Props) {
// ...
}
// При dynamicParams = false:
// /blog/hello-world → ✅ статическая страница
// /blog/new-post → ❌ 404 (не в generateStaticParams)
// При dynamicParams = true (default):
// /blog/hello-world → ✅ статическая (из кеша)
// /blog/new-post → ✅ рендерится динамически, затем кешируется

Окно терминала
$ npm run build
Route (app) Size First Load JS
/ 142 B 87.1 kB
/about 180 B 87.1 kB
/blog 320 B 87.3 kB
/blog/[slug] 275 B 87.3 kB
/blog/hello-world
/blog/nextjs-guide
/blog/typescript-tips
/contact 200 B 87.1 kB
(Static) prerendered as static content
(SSG) prerendered as static HTML (uses generateStaticParams)
ƒ (Dynamic) server-rendered on demand
Generating static pages (8/8)
Collecting page data
Finalizing page optimization

// ✅ ОТЛИЧНО для SSG:
// 1. Блог / статьи — контент редко меняется
export async function generateStaticParams() {
const posts = await cms.getPosts(); // Обычно десятки/сотни постов
return posts.map(p => ({ slug: p.slug }));
}
// 2. Документация — очень стабильный контент
export async function generateStaticParams() {
const docs = await getAllDocs();
return docs.map(d => ({ slug: d.path.split('/') }));
}
// 3. Страницы товаров (e-commerce) — тысячи товаров
export async function generateStaticParams() {
const products = await db.product.findMany({
select: { slug: true },
where: { published: true },
});
return products.map(p => ({ slug: p.slug }));
}
// 4. Лендинги, маркетинговые страницы
// ✅ Максимальная производительность
// 5. Страницы профилей (если их немного)
export async function generateStaticParams() {
const users = await db.user.findMany({
select: { username: true },
where: { public: true },
take: 1000, // Ограничиваем при сборке
});
return users.map(u => ({ username: u.username }));
}
// ❌ НЕ подходит для SSG:
// 1. Персонализированный контент (зависит от пользователя)
// 2. Данные, меняющиеся чаще чем раз в минуту
// 3. Страницы с авторизацией (корзина, профиль с приватными данными)
// 4. Реальный-тайм данные (чаты, live трекинг)
// 5. Поисковые результаты (infinite combinations)

АспектSSGSSRISRCSR
Когда рендеритПри сборкеПри запросеПри сборке + периодическиВ браузере
Скорость⚡⚡⚡⚡⚡⚡⚡⚡
Свежесть данныхПри сборкеВсегда свежиеНастраиваетсяВсегда свежие
SEO✅✅✅✅✅✅
Нагрузка на серверМинимальнаяВысокаяНизкаяМинимальная
CDN-friendy✅✅✅✅
ДеплойПростойНужен серверНужен серверЛюбой

// 1. Генерация только популярных страниц при сборке
// Остальные — по требованию (dynamicParams = true)
export async function generateStaticParams() {
// Только топ-100 самых популярных постов
const posts = await db.post.findMany({
orderBy: { views: 'desc' },
take: 100,
select: { slug: true },
});
return posts.map(p => ({ slug: p.slug }));
}
// Новые посты: генерируются при первом запросе и кешируются
// 2. Параллельная генерация данных
export async function generateStaticParams() {
// Запускаем все источники данных параллельно
const [enPosts, ruPosts, dePosts] = await Promise.all([
getPostsByLang('en'),
getPostsByLang('ru'),
getPostsByLang('de'),
]);
return [
...enPosts.map(p => ({ lang: 'en', slug: p.slug })),
...ruPosts.map(p => ({ lang: 'ru', slug: p.slug })),
...dePosts.map(p => ({ lang: 'de', slug: p.slug })),
];
}
// 3. Частичная предгенерация + ISR
export const revalidate = 3600; // Обновлять статику каждый час
export async function generateStaticParams() {
// Генерируем при сборке
return await getPopularSlugs();
}
// Остальные — на лету, с кешем на 1 час

next.config.mjs
// Экспорт статического сайта (без сервера!)
const nextConfig = {
output: 'export', // Генерирует static HTML/CSS/JS
// trailingSlash: true, // /about → /about/index.html
};
// При npm run build создаст папку 'out' со статическими файлами!
// Можно деплоить на GitHub Pages, S3, Netlify без сервера
// Ограничения output: 'export':
// ❌ API Routes (нет сервера)
// ❌ Server Actions
// ❌ Middleware (кроме edge)
// ❌ ISR (нет сервера для регенерации)
// ✅ SSG → ✅
// ✅ Client Components → ✅

  1. SSG по умолчанию — компонент без динамических API = статичный
  2. generateStaticParams — список params для предгенерации
  3. dynamicParamsfalse = 404 для неизвестных, true = динамический рендеринг
  4. cache: ‘force-cache’ — все fetch() кешируются при сборке
  5. output: ‘export’ — полностью статический сайт (без сервера)
  6. Идеально для: блоги, документация, лендинги, каталоги товаров