Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як працює генерація статичних сайтів (SSG) у Next.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**SSG (Static Site Generation, генерація статичних сайтів)** у Next.js генерує HTML під час `next build` і роздає його з CDN без серверних обчислень на кожен запит. ```tsx const data = await fetch('https://api.example.com/post', { cache: 'force-cache' // виконується один раз під час збірки }).then(r => r.json()); ``` **Головне:** рендеринг переноситься з часу запиту на час збірки. TTFB падає до ~10ms з CDN проти 100-500ms для SSR. Якщо дані оновлюються після деплою, використовуй ISR з `export const revalidate = 60`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**SSG (Static Site Generation, генерація статичних сайтів)** у Next.js перетворює сторінки на HTML-файли під час `next build` і роздає їх з CDN без будь-якого серверного коду на кожен запит. ## Теорія ### TL;DR - SSG: випічка всіх тістечок до відкриття магазину - відвідувач бере готове, без очікування - Головна відмінність від SSR: рендеринг відбувається під час збірки, а не на кожен запит - TTFB: ~10ms з CDN проти 100-500ms для SSR - Використовуй SSG коли контент оновлюється рідше ніж раз на день і важливі швидкість або SEO - `cookies()` або `searchParams` у компоненті автоматично виключать сторінку з SSG ще під час збірки ### Швидкий приклад ```tsx // app/page.tsx - виконується один раз під час збірки, результат - статичний HTML export default async function HomePage() { const data = await fetch('https://api.example.com/posts/1', { cache: 'force-cache' // фіксуємо відповідь на момент збірки }).then(res => res.json()); return ( <div> <h1>{data.title}</h1> <p>{data.body}</p> </div> ); } // Результат: .next/server/app/index.html з даними всередині // На запит: CDN повертає HTML за ~10ms, далі JS гідратує клієнт ``` Цей компонент запускається один раз під час `next build`. Відповідь API вбудована в HTML. При запиті сервер не задіяний взагалі. ### Час збірки проти часу запиту SSR запускає Node.js на кожен запит. SSG запускає його рівно один раз. Саме ця різниця дає SSG ~10ms TTFB з CDN, тоді як SSR потребує 100-500ms на roundtrip до сервера. Компроміс: дані в SSG заморожені до наступної збірки або до спрацювання ISR. ### Коли використовувати SSG - Документація, блоги, маркетингові сторінки: стабільний контент, важливі SEO та можливі пікові навантаження - Каталоги товарів з тижневими оновленнями: SSG з ISR (`revalidate: 60`) закриє це завдання - Високий трафік при невеликому бюджеті на сервер: SSG повністю усуває витрати на обчислення в рантаймі - Дашборди з персоналізованими даними: SSG тут не підійде, краще SSR або CSR - Дані в реальному часі (ціни акцій, чати): SSG не варіант ### Таблиця порівняння | | SSG | SSR | CSR | |---|---|---|---| | **Час рендерингу** | `next build` | Кожен запит | Браузер | | **TTFB** | ~10ms (CDN) | 100-500ms | 2-5s (JS-бандл) | | **Витрати на сервер** | Нуль після деплою | Залежать від трафіку | Мінімальні (статичний хост) | | **SEO** | Повний HTML відразу | Повний HTML відразу | Слабке (потребує гідратації) | | **Свіжість даних** | Заморожені до ребілду або ISR | Завжди свіжі | За запитом | | **API у Next.js** | Дефолт App Router / `getStaticProps` | `getServerSideProps` | `useEffect` | | **Для чого** | Доки, блоги (Vercel.com) | Дашборди (Stripe.com) | SPA (Gmail) | ### Як Next.js будує статичні сторінки Під час `next build` Next.js сканує всі сторінки, запускає React Server Components у Node.js, отримує дані через `fetch` з `cache: 'force-cache'`, серіалізує React-дерево в HTML за допомогою `renderToPipeableStream` і записує `.html`-файли в `.next/server/app`. Після цього сервер у запитах не задіяний: CDN роздає все самостійно. Для динамічних маршрутів `generateStaticParams` вказує збірнику, які шляхи потрібно попередньо згенерувати: ```tsx // app/blog/[slug]/page.tsx export async function generateStaticParams() { const posts = await fetch('https://api.example.com/posts').then(r => r.json()); return posts.map((post: { slug: string }) => ({ slug: post.slug })); } export default async function BlogPost({ params }: { params: { slug: string } }) { const post = await fetch(`https://api.example.com/posts/${params.slug}`, { cache: 'force-cache' }).then(r => r.json()); return ( <article> <h1>{post.title}</h1> <p>{post.content}</p> </article> ); } // Збірка генерує: /blog/my-first-post/index.html, /blog/next-tips/index.html, ... ``` Без `generateStaticParams` на динамічному маршруті Next.js переходить на SSR. Це поширена регресія продуктивності, яку команди помічають пізно. ### ISR: статичні сторінки, що оновлюються ISR (Incremental Static Regeneration, інкрементальна статична регенерація) дозволяє ревалідувати сторінку у фоні без повного ребілду. Додай `export const revalidate = 60` щоб оновлювати кожні 60 секунд: ```tsx // app/blog/[slug]/page.tsx export const revalidate = 60; // фонова ревалідація кожні 60s export default async function BlogPost({ params }: { params: { slug: string } }) { const post = await fetch(`https://api.example.com/posts/${params.slug}`, { next: { revalidate: 60 } }).then(r => r.json()); return ( <article> <h1>{post.title}</h1> <div dangerouslySetInnerHTML={{ __html: post.content }} /> </article> ); } // Перший запит після 60s: повертає старий HTML, паралельно регенерує // Другий запит: отримує свіжий HTML ``` ISR робить контент типу "переважно статичний" практичним. Документація Stripe API Reference використовує цей підхід: сторінки залишаються актуальними без повного ребілду. ### Типові помилки **`fetch` без `cache: 'force-cache'` в App Router** ```tsx // Неправильно: у деяких конфігураціях Next.js 14+ дефолт = no-store const data = await fetch('/api/data'); // Правильно const data = await fetch('/api/data', { cache: 'force-cache' }); // Або на рівні сторінки: export const dynamic = 'force-static'; ``` Без `force-cache` сторінка стає динамічним рендером. Логи збірки покажуть `⚠ Dynamic`, і CDN нічого не кешує. **Відсутній `generateStaticParams` для динамічних маршрутів** ```tsx // app/shop/[id]/page.tsx без generateStaticParams // Результат: SSR-fallback, нуль попередньо згенерованого HTML ``` Кожен динамічний маршрут без `generateStaticParams` автоматично стає SSR. Додай його або усвідомлено прийми цей компроміс. **Використання `cookies()` у нібито статичному компоненті** ```tsx // Компонент виглядає статичним, але це не так export default async function Dashboard({ params }: { params: { id: string } }) { const isLoggedIn = !!cookies().get('session')?.value; // примушує динамічний рендер return <div>User {params.id}</div>; } ``` `cookies()` позначає сторінку як `dynamic = 'force-dynamic'` під час аналізу збірки. Сторінка стає SSR незалежно від наявності `generateStaticParams`. Перевіряй логи `next build` на попередження "Dynamic Function". Мені доводилося бачити, як це дивує команди, які вже налаштували `generateStaticParams` і вважали, що все гаразд. **`revalidate: 0` в ISR** Значення `revalidate: 0` повністю вимикає статичну генерацію. Використовуй `revalidate: false` для чистого SSG або будь-яке додатне число для ISR. **Мутація даних у `getStaticProps` (Pages Router)** ```tsx // Неправильно export async function getStaticProps() { const data = await fetch('/api').then(r => r.json()); data.count++; // мутація ігнорується або ламає збірку return { props: { data } }; } // Правильно export async function getStaticProps() { const data = await fetch('/api').then(r => r.json()); return { props: { data: { ...data, count: data.count + 1 } } }; } ``` `getStaticProps` має бути чистою функцією. Трансформуй дані без мутацій. ### Де використовується на практиці - Vercel.com docs: SSG з ISR, 10M+ відвідувань на місяць без серверних обчислень на запит - TailwindUI.com: `generateStaticParams` попередньо будує 500+ сторінок із прев'ю компонентів - Stripe.com docs: SSG + ISR для оновлення API Reference без повного ребілду - TinaCMS + Next.js: headless CMS тригерить ребілд при зміні контенту, класичний Jamstack-підхід ### Питання на співбесіді **Q:** Як Next.js вирішує, чи рендерити сторінку статично, чи динамічно? **A:** Під час `next build` він аналізує кожну сторінку на наявність динамічних функцій: `cookies()`, `headers()`, `searchParams`. Якщо знаходить, позначає сторінку `force-dynamic`. Вивід збірки показує `○ (Static)` або `λ (Dynamic)` для кожного маршруту. **Q:** Яка різниця між `export const revalidate = 60` і `next: { revalidate: 60 }` у fetch? **A:** Рівень сторінки встановлює дефолт для всього маршруту. Рівень fetch перевизначає для конкретного запиту і має пріоритет. В одній сторінці можна мати один запит з довгою ревалідацією і один з короткою. **Q:** Чому `cookies()` запобігає SSG? **A:** Це сигнал, що сторінка залежить від контексту конкретного запиту. Next.js не може попередньо згенерувати щось, що відрізняється для кожного користувача, тому позначає її `dynamic = 'force-dynamic'` і пропускає статичну генерацію. **Q:** ISR проти повного ребілду: коли що обирати? **A:** ISR регенерує окремі сторінки у фоні без простою. Повний ребілд зачіпає все відразу і швидший для невеликих сайтів. Для 50 сторінок підійде повний ребілд. Для 10 000 сторінок товарів ISR є єдиним практичним варіантом. **Q:** Що відбувається з маршрутами, не переліченими в `generateStaticParams`? **A:** За замовчуванням (`dynamicParams = true`) для невідомих шляхів використовується SSR-fallback. Встанови `export const dynamicParams = false` щоб повертати 404 для всього, що не було попередньо побудовано. ## Приклади ### Базова SSG-сторінка (App Router) ```tsx // app/docs/page.tsx export default async function DocsPage() { const docs = await fetch('https://api.itlead.org/docs', { cache: 'force-cache' }).then(res => res.json()); return ( <ul> {docs.map((doc: { slug: string; title: string }) => ( <li key={doc.slug}>{doc.title}</li> ))} </ul> ); } // Результат збірки: docs/index.html з усіма заголовками всередині // CDN роздає файл напряму, Node.js не задіяний ``` `cache: 'force-cache'` фіксує відповідь на момент збірки. Це найпростіше налаштування SSG в App Router. ### Динамічні маршрути з `generateStaticParams` ```tsx // app/docs/[slug]/page.tsx export async function generateStaticParams() { const docs = await fetch('https://api.itlead.org/docs').then(r => r.json()); return docs.map((doc: { slug: string }) => ({ slug: doc.slug })); } export default async function DocPage({ params }: { params: { slug: string } }) { const doc = await fetch(`https://api.itlead.org/docs/${params.slug}`, { cache: 'force-cache' }).then(res => res.json()); return ( <article> <h1>{doc.title}</h1> <div>{doc.content}</div> </article> ); } ``` `generateStaticParams` запускається один раз під час збірки, повертає всі slug-и, і Next.js генерує по одному HTML-файлу на кожен. В рантаймі: чистий CDN, нуль серверів. ### Pages Router з `getStaticProps` і `getStaticPaths` ```tsx // pages/docs/[slug].tsx export async function getStaticPaths() { const docs = await fetch('https://api.itlead.org/docs').then(r => r.json()); return { paths: docs.map((doc: { slug: string }) => ({ params: { slug: doc.slug } })), fallback: false // 404 для будь-якого невідомого slug }; } export async function getStaticProps({ params }: { params: { slug: string } }) { const doc = await fetch(`https://api.itlead.org/docs/${params.slug}`).then(r => r.json()); return { props: { doc } }; } export default function DocPage({ doc }: { doc: { title: string; content: string } }) { return ( <article> <h1>{doc.title}</h1> <p>{doc.content}</p> </article> ); } ``` `fallback: false` означає, що невідомі slug-и повертають 404 одразу. Використовуй `fallback: 'blocking'` якщо потрібен SSR-fallback для нового контенту, доданого після збірки.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.