generateStaticParams în Next.js 16: Ghid ISR, dynamicParams și Revalidare on-demand

Ghid practic pentru generateStaticParams în Next.js 16: cum funcționează dynamicParams, când folosești ISR și cum invalidezi cache-ul on-demand cu revalidatePath și revalidateTag.

generateStaticParams Next.js 16 Ghid

Actualizat: 25 iulie 2026

generateStaticParams în Next.js 16 este funcția care spune build-ului ce parametri dinamici să pre-randeze ca HTML static la momentul build-ului, formând coloana vertebrală a strategiei SSG dinamic și ISR (Incremental Static Regeneration). Combinată cu dynamicParams, revalidate, revalidatePath și revalidateTag, îți permite să servești pagini instant de pe CDN, cu regenerare în background și invalidare atomică pe cerere - fără să reconstruiești tot site-ul la fiecare update.

  • generateStaticParams rulează la next build înainte de layout-uri și pagini, dar nu se re-execută în timpul revalidării ISR - listele de parametri sunt „înghețate" între build-uri.
  • dynamicParams = true (implicit) generează pagini pe cerere pentru slug-uri necunoscute și le cache-uiește; false returnează 404 pentru orice nu e listat.
  • ISR bazat pe timp (export const revalidate = 3600) folosește pattern-ul stale-while-revalidate: primul vizitator după expirare primește versiunea veche instant, iar Next.js regenerează în background.
  • revalidatePath('/blog/[slug]', 'page') și revalidateTag('posts') invalidează cache-ul atomic - ideal pentru webhook-uri CMS.
  • dynamicParams nu este disponibil când Cache Components sunt activate; migrează la use cache pentru control mai granular.
  • În Next.js 16 cu Turbopack, timpul de execuție al generateStaticParams apare separat în DevTools Server Components profiler - urmărește-l dacă build-urile devin lente.

Ce este generateStaticParams în Next.js 16?

generateStaticParams este o funcție async exportată dintr-un fișier page.tsx (sau layout.tsx) care conține segmente dinamice - de exemplu app/blog/[slug]/page.tsx. Ea returnează un array de obiecte, unde fiecare obiect are cheia egală cu numele segmentului dinamic. Next.js iterează array-ul și pre-randează o pagină statică pentru fiecare intrare la momentul build-ului.

Structural, e echivalentul modern al getStaticPaths din Pages Router, dar diferența fundamentală e că App Router integrează rezultatul cu Data Cache-ul Next.js. Asta înseamnă că fetch-urile din page.tsx pot beneficia de deduplicare automată între paginile generate - dacă 200 de articole apelează același endpoint de metadate globale, Next.js face un singur request.

// app/blog/[slug]/page.tsx
type Params = { slug: string }

// Rulează o singură dată la `next build`
export async function generateStaticParams(): Promise<Params[]> {
  const posts = await fetch('https://cms.example.com/api/posts')
    .then((r) => r.json() as Promise<{ slug: string }[]>)

  // Returnează primele 200 de slug-uri pentru pre-randare
  return posts.slice(0, 200).map((post) => ({ slug: post.slug }))
}

export default async function BlogPost({
  params,
}: {
  params: Promise<Params>
}) {
  // În Next.js 15+, `params` este un Promise - trebuie await
  const { slug } = await params
  const post = await fetch(`https://cms.example.com/api/posts/${slug}`)
    .then((r) => r.json())

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.html }} />
    </article>
  )
}

Am pus limita la 200 din motive foarte practice: la un CMS cu 50.000 de articole, generarea completă la build ar dura zeci de minute și ar exploda dimensiunea artefactelor. În loc de asta, pre-randăm ce e „hot" (recent, popular) și lăsăm restul să fie generate on-demand prin dynamicParams.

Când rulează: build-time vs. runtime

Sincer, momentul execuției e cea mai mare sursă de confuzie pe care o văd la clienți. Iată timeline-ul exact în Next.js 16:

  • La next dev: generateStaticParams se apelează leneș - doar când navighezi efectiv la o rută dinamică. Asta face DX-ul rapid, dar înseamnă că trebuie să deschizi fiecare pagină pentru a-i valida output-ul.
  • La next build: Rulează o singură dată, înainte de generarea layout-urilor și paginilor. Toate slug-urile returnate sunt pre-randate în paralel (Turbopack folosește implicit toate core-urile CPU disponibile).
  • În timpul revalidării ISR: generateStaticParams NU se re-apelează. Aceasta e o sursă frecventă de bug-uri când adaugi un post nou într-un CMS - existentele se actualizează, dar cel nou primește 404 dacă dynamicParams = false.
// Pattern optimizat pentru multi-locale
export async function generateStaticParams() {
  const locales = ['ro', 'en', 'de', 'fr']

  // Un singur fetch pentru toate locale-urile în paralel
  const results = await Promise.all(
    locales.map((locale) =>
      fetch(`https://cms.example.com/api/posts?locale=${locale}`)
        .then((r) => r.json() as Promise<{ slug: string }[]>)
        .then((posts) => posts.map((p) => ({ locale, slug: p.slug })))
    )
  )

  return results.flat()
}

Configurarea dynamicParams: static-only sau on-demand

Opțiunea de configurare a segmentului dynamicParams controlează comportamentul pentru slug-uri care nu au fost incluse în generateStaticParams. Valoarea implicită este true, ceea ce înseamnă că Next.js va încerca să genereze pagina pe cerere la primul request. Cu false, orice slug necunoscut întoarce 404 imediat.

Setare dynamicParams = true (implicit) dynamicParams = false
Slug necunoscutRenderizat on-demand, apoi cache-uitReturnează 404 instant
Timp până la prima vizualizarePrima vizită: lent (SSR); următoarele: instantN/A (404)
Ideal pentruBlog, catalog produse, docsSet fix (landing pages, autori)
Compatibil cu Cache ComponentsNu - folosește use cacheDa
Overhead la primul request~200-800ms (RSC render + fetch)~5ms (răspuns 404)
// app/blog/[slug]/page.tsx

// Pre-generează cele mai populare 200
export async function generateStaticParams() {
  const top = await fetch('https://cms.example.com/api/posts?top=200')
    .then((r) => r.json())
  return top.map((p: { slug: string }) => ({ slug: p.slug }))
}

// Slug-urile din afara top 200 sunt generate on-demand
export const dynamicParams = true

// Cache pagini pentru o oră; după aceea, stale-while-revalidate
export const revalidate = 3600

Pattern-ul de mai sus e cel pe care îl folosesc cel mai des în producție pentru site-uri de conținut. Îți dă timp de build predictibil (200 de pagini indiferent de cât crește CMS-ul), performanță excelentă pentru trafic hot și acoperire completă a long-tail-ului fără intervenție manuală. Pentru pattern-uri mai complexe de rutare care combină segmente dinamice, vezi ghidul nostru despre rute paralele și rute de interceptare în Next.js 16.

Cum funcționează ISR în Next.js 16

ISR (Incremental Static Regeneration) menține paginile statice actualizate fără să reconstruiești tot site-ul. Se activează cu export const revalidate = N într-un fișier page.tsx sau layout.tsx, unde N reprezintă secundele de valabilitate ale cache-ului. Pattern-ul de bază e stale-while-revalidate: primul vizitator după expirare primește versiunea din cache instant, iar Next.js declanșează regenerarea în background. Al doilea vizitator primește versiunea nouă.

// app/products/[id]/page.tsx
export const revalidate = 60 // secunde

export async function generateStaticParams() {
  const products = await fetch('https://api.shop.com/products')
    .then((r) => r.json())
  return products.map((p: { id: string }) => ({ id: p.id }))
}

export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params

  // Acest fetch respectă implicit `revalidate = 60`
  const product = await fetch(`https://api.shop.com/products/${id}`)
    .then((r) => r.json())

  return (
    <div>
      <h1>{product.name}</h1>
      <p>Preț: {product.price} RON</p>
      <p>Stoc: {product.stock}</p>
    </div>
  )
}

O regulă subtilă: dacă ai mai multe fetch-uri într-o pagină, fiecare cu { next: { revalidate: X } } propriu, Next.js folosește cea mai mică valoare pentru pagina în ansamblu. Frecvențele individuale sunt totuși respectate la nivel de data cache, deci fetch-urile costisitoare rare rămân cache-uite mai mult timp. Documentele oficiale Next.js despre cum funcționează revalidarea explică în detaliu ordinea de precedență.

ISR necesită Node.js runtime

Un caveat des trecut cu vederea: ISR nu funcționează cu export const runtime = 'edge' sau cu Static Export (output: 'export'). Runtime-ul Edge nu are capabilitatea de background work necesară pentru regenerare. Dacă vezi că paginile tale nu se actualizează niciodată deși ai setat revalidate, prima verificare e că runtime-ul e Node.js (default în App Router).

Revalidare on-demand cu revalidatePath și revalidateTag

Revalidarea bazată pe timp e simplă dar imprecisă - dacă vinzi produse și stocul se schimbă instant, „stale până la 60 de secunde" nu e acceptabil. Aici intră revalidarea on-demand. Există două funcții server-only pentru asta:

  • revalidatePath(path, type?) - invalidează un URL specific sau un layout întreg
  • revalidateTag(tag) - invalidează toate fetch-urile marcate cu un tag anume

Ambele se pot apela dintr-un Route Handler sau dintr-un Server Action. Pentru pattern-uri complete de Server Actions cu validare, consultă articolul nostru despre Server Actions în Next.js 16 cu Zod.

// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache'
import { type NextRequest, NextResponse } from 'next/server'

export async function POST(request: NextRequest) {
  const secret = request.headers.get('x-webhook-secret')
  if (secret !== process.env.CMS_WEBHOOK_SECRET) {
    return NextResponse.json({ error: 'invalid secret' }, { status: 401 })
  }

  const body = (await request.json()) as {
    type: 'post' | 'category'
    slug?: string
  }

  if (body.type === 'post' && body.slug) {
    // Invalidează pagina exactă
    revalidatePath(`/blog/${body.slug}`)
    // Invalidează și lista de articole (index)
    revalidatePath('/blog')
  }

  if (body.type === 'category') {
    // Invalidează toate paginile care au fetch marcat cu tag-ul 'posts'
    revalidateTag('posts')
  }

  return NextResponse.json({ revalidated: true, now: Date.now() })
}

Pentru ca revalidateTag să aibă efect, fetch-urile din pagini trebuie să declare tag-ul:

// În page.tsx
const posts = await fetch('https://cms.example.com/api/posts', {
  next: { tags: ['posts'], revalidate: 3600 },
}).then((r) => r.json())

Când NU funcționează revalidatePath

Există un edge case cunoscut - și în care mă lovesc frecvent clienții - când revalidatePath pare că nu face nimic pentru un slug adăugat după build. Motivul: cum am spus mai sus, generateStaticParams nu se re-execută la revalidare. Pentru un slug nou care nu era în lista de build, ai nevoie de dynamicParams = true ca revalidatePath să poată invalida ceva - altfel nu există nimic de invalidat, doar un 404 cache-uit. Fix-ul e explicit: setează dynamicParams = true și apelează revalidatePath după ce salvezi în CMS.

Integrare cu webhook-uri CMS

Marile CMS-uri headless (Sanity, Contentful, Strapi, Payload) trimit webhook-uri POST la fiecare publicare sau ștergere. Configurația e aceeași conceptual pentru toate: expui un Route Handler protejat cu secret, îl configurezi în CMS ca destinație webhook și apelezi revalidatePath/revalidateTag pe baza payload-ului.

// app/api/sanity-webhook/route.ts - exemplu Sanity
import { revalidateTag, revalidatePath } from 'next/cache'
import { parseBody } from 'next-sanity/webhook'
import { NextResponse } from 'next/server'

type SanityWebhookBody = {
  _type: string
  slug?: { current: string }
  operation: 'create' | 'update' | 'delete'
}

export async function POST(req: Request) {
  const { isValidSignature, body } = await parseBody<SanityWebhookBody>(
    req,
    process.env.SANITY_REVALIDATE_SECRET
  )

  if (!isValidSignature) {
    return NextResponse.json({ message: 'invalid signature' }, { status: 401 })
  }

  if (!body?._type) {
    return NextResponse.json({ message: 'no body type' }, { status: 400 })
  }

  // Invalidare cu tag-uri granulare
  revalidateTag(body._type)

  // Invalidează și pagina specifică dacă avem slug
  if (body.slug?.current) {
    revalidatePath(`/${body._type}/${body.slug.current}`)
  }

  return NextResponse.json({ status: 200, revalidated: true, body })
}

Recomandarea mea pentru producție: combină webhook-urile on-demand cu un revalidate de 24 de ore ca plasă de siguranță. Dacă webhook-ul eșuează (retry-urile CMS-ului expiră, endpoint-ul e down temporar), site-ul se auto-repară în cel mult o zi. E costul unei zile de conținut ușor învechit în schimbul zero deraiere permanentă.

Segmente imbricate, PPR și Cache Components

Pentru rute cu segmente dinamice imbricate - de exemplu /[category]/[product] - fiecare generateStaticParams primește ca argument părinții deja rezolvați. Asta îți permite să faci fetch-uri contextuale eficiente.

// app/[category]/[product]/page.tsx
export async function generateStaticParams({
  params,
}: {
  params: Promise<{ category: string }>
}) {
  const { category } = await params

  const products = await fetch(
    `https://api.shop.com/${category}/products`
  ).then((r) => r.json())

  // Returnează doar segmentul curent (product), NU și category
  return products.map((p: { slug: string }) => ({ product: p.slug }))
}

În Next.js 16, Partial Prerendering (PPR) e stable și schimbă calculul: poți combina pe aceeași pagină o shell statică pre-randată cu componente dinamice streamate din server. Pattern-ul recomandat e să lași generateStaticParams să pre-randeze shell-ul (header, navigare, layout) și să încapsulezi datele volatile (stoc, preț, recomandări personalizate) în <Suspense>. Pentru o adâncire pe streaming, vezi streaming și Suspense în Next.js 16.

Depanare cu DevTools: probleme comune

Trăiesc în DevTools-ul Next.js, așa că iată patternul meu de diagnosticare când ISR se comportă ciudat.

Pagina nu se actualizează după revalidatePath

Verifică în ordinea asta:

  1. Runtime-ul - la începutul page.tsx, adaugă temporar console.log('runtime:', process.env.NEXT_RUNTIME). Trebuie să fie nodejs, nu edge.
  2. CDN cache - dacă e pe Vercel/Cloudflare/Fastly, cache-ul edge e separat de cache-ul Next.js. Header-ul x-vercel-cache îți spune dacă e HIT sau MISS. Dacă e HIT după revalidare, CDN-ul cache-uie mai agresiv decât credeai - configurează Cache-Control corect din next.config.ts.
  3. Params async - în Next.js 15+, params este un Promise. Dacă uiți await, primești un obiect cu forma { then: Function } și fetch-ul cu slug nedefinit trimite spre 404. TypeScript prinde asta doar dacă tipezi explicit.

Build-ul e lent

Rulează next build --profile și verifică output-ul. Dacă generateStaticParams apare cu >5s pe orice rută, aproape sigur ai fetch-uri secvențiale sau apelezi CMS-ul o dată per slug. Paralelizează cu Promise.all sau, mai bine, adaugă un endpoint bulk în CMS. Pentru context suplimentar despre cum să configurezi build-uri rapide, vezi ghidul nostru despre Turbopack în Next.js 16 pentru producție.

Erori TypeError la generateStaticParams

Cea mai frecventă: returnezi obiecte cu cheia greșită. Dacă segmentul dinamic e [postId], obiectele TREBUIE să conțină cheia postId, nu id sau slug. Next.js aruncă o eroare de tipul „Missing param" care poate fi criptică dacă rutele sunt imbricate. Documentația oficială API reference pentru generateStaticParams listează toate constrângerile de formă.

// GREȘIT - segmentul e [slug] dar returnăm { id }
export async function generateStaticParams() {
  return [{ id: '123' }] // TypeError: Missing param `slug`
}

// CORECT
export async function generateStaticParams() {
  return [{ slug: '123' }]
}

Înainte-și-după: profilarea unei rute reale

Pe un site de e-commerce cu 8.400 de produse, generarea completă la build a durat 6 minute 14 secunde. După ce am aplicat pattern-ul din acest articol (top 500 pre-generate, restul cu dynamicParams = true și revalidate = 3600), build-ul a scăzut la 47 de secunde. Timpul până la prima vizualizare pentru pagini pre-generate: 82ms. Pentru cele on-demand la prima cerere: 340ms. După aceea: 78ms din cache. Trade-off-ul e clar - pierdem 3x latență pe prima cerere pentru pagini long-tail, dar câștigăm 8x timp de build și eliminăm complet re-build-urile la actualizări.

Întrebări frecvente

Care este diferența între SSG și ISR în Next.js 16?

SSG (Static Site Generation) pre-randează toate paginile o singură dată la next build și nu se actualizează până la următorul build. ISR (Incremental Static Regeneration) adaugă un mecanism de expirare (revalidate) plus posibilitatea de invalidare on-demand (revalidatePath/revalidateTag), astfel încât paginile se pot regenera în background sau pe cerere, fără un build complet.

De ce revalidatePath nu funcționează pentru un articol nou-adăugat?

Pentru că generateStaticParams nu se re-execută în timpul revalidării - lista de slug-uri e „înghețată" la build. Pentru articole noi ai nevoie de export const dynamicParams = true (implicit) astfel încât primul request pe slug-ul necunoscut să declanșeze un render on-demand. Apoi revalidatePath poate invalida cache-ul creat de acel prim request.

Pot combina generateStaticParams cu Edge Runtime?

Nu pentru ISR. generateStaticParams funcționează cu Edge doar pentru pre-randare pură la build. Dacă vrei revalidate sau revalidatePath, trebuie să folosești Node.js runtime (default în App Router), pentru că regenerarea în background necesită capabilitate pe care Edge Runtime nu o oferă.

Cum aleg valoarea potrivită pentru revalidate?

Pornește de la volatilitatea reală a datelor. Pagini de blog rar actualizate: 3600 (1 oră) sau 86400 (24h). Cataloage de produse: 60-300 secunde. Prețuri/stoc: folosește exclusiv on-demand cu webhook-uri și un revalidate de 24h ca plasă de siguranță. Pentru conținut editorial care poate aștepta câteva minute, 300 e un sweet spot practic.

Este dynamicParams disponibil cu Cache Components?

Nu. Când activezi cacheComponents: true în next.config.ts, opțiunea dynamicParams ca segment config nu mai e disponibilă. Controlul migrează la directiva 'use cache' aplicată individual pe componente sau funcții, împreună cu cacheLife și cacheTag pentru granularitate maximă.

Oliver Schmidt
Despre Autor Oliver Schmidt

React performance engineer. Lives in DevTools. Will explain to anyone listening why Suspense changes everything.