Next.js 16 Cache Components és Partial Prerendering: A gyorsítótárazás új korszaka

Ismerd meg a Next.js 16 Cache Components és PPR rendszerét a gyakorlatban: use cache direktíva, cacheLife, cacheTag, updateTag és revalidateTag használata valós kódpéldákkal.

Next.js 16 Cache Components + PPR Útmutató

Bevezetés: Miért változtatja meg mindent a Cache Components?

Ha valaha is küzdöttél a Next.js App Router gyorsítótárazási rendszerével — és őszintén, ki ne tette volna —, akkor pontosan tudod, miről beszélek. A korábbi verziókban a gyorsítótárazás implicit és kiszámíthatatlan volt: a fetch hívások automatikusan cache-elődtek, a route-ok maguktól statikussá váltak, és sokszor fogalmad sem volt, miért nem frissül az oldal. A fejlesztői frusztráció akkora lett, hogy a Next.js csapat végül teljesen újragondolta az egész megközelítést.

Na és itt jön a képbe a Next.js 16.

A 2025 októberében megjelent verzió gyökeresen más utat választ. A Cache Components és a Partial Prerendering (PPR) együtt alkotja azt az architektúrát, amivel végre búcsút inthetsz a „minden vagy semmi" renderelési döntéseknek. Mostantól egyetlen oldalon belül keverheted a statikus, gyorsítótárazott és dinamikus tartalmat — mégpedig explicit, átlátható módon.

Ebben az útmutatóban végigmegyünk a Cache Components minden fontos részletén: a "use cache" direktívától a cacheLife és cacheTag API-kon át az updateTag és revalidateTag frissített működéséig. Ha már olvastad az adatlekérdezésről és a Server Actions-ről szóló korábbi cikkeinket, ez itt a kirakós utolsó darabja — az a réteg, ami mindent összeköt.

A régi rendszer problémái: Miért kellett változtatni?

Az implicit gyorsítótárazás csapdái

A Next.js 15-ben és korábbi verziókban a gyorsítótárazás alapértelmezetten be volt kapcsolva. Első hallásra remekül hangzik — gyors oldalak, minimális szerver terhelés. A gyakorlatban viszont rengeteg fejfájást okozott:

  • Kiszámíthatatlan viselkedés — Nem volt mindig egyértelmű, hogy egy adott fetch hívás cache-elődik-e vagy sem.
  • Minden vagy semmi renderelés — Elég volt egyetlen komponens, ami dinamikus adatot igényelt (mondjuk egy személyre szabott üdvözlés a fejlécben), és az egész oldal dinamikussá vált.
  • Nehéz hibakeresés — A fejlesztők gyakran nem tudták eldönteni, hogy a cache-ből vagy friss adatból kapják-e az eredményt. Ez nem vicc.
  • Konfiguráció-labirintus — A force-dynamic, force-static, revalidate, fetchCache és társaik kombinálása egyre bonyolultabb lett.

A Next.js 16 válasza: Explicit gyorsítótárazás

A Next.js 16 megfordítja az alapértelmezést: minden dinamikus, hacsak nem mondod meg kifejezetten, hogy legyen gyorsítótárazva. Ez sokkal kiszámíthatóbb viselkedést eredményez. A "use cache" direktíva révén pontosan te döntöd el, mit és meddig cache-elsz — nem a framework találgatja ki helyetted.

A Cache Components bekapcsolása és alapjai

Konfiguráció

A Cache Components használatba vételéhez egyetlen konfigurációs sor kell a next.config.ts fájlodba:

// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  cacheComponents: true,
};

export default nextConfig;

Ennyi. Ez a flag egyszerre engedélyezi a Cache Components-et és a Partial Prerendering-et (PPR). A korábbi experimental.ppr és experimental.dynamicIO flageket eltávolították — mostantól ez az egyetlen beállítás, amire szükséged van.

Hogyan működik a renderelés?

Amikor a cacheComponents engedélyezve van, a Next.js a következőképpen dolgozza fel az oldalad:

  1. Build időben — Végigmegy a komponens fán. Ami nem igényel hálózati kérést vagy futásidejű adatot, az automatikusan bekerül a statikus héjba (static shell).
  2. Dinamikus tartalom kezelése — Ha egy komponens hálózati kérést indít vagy futásidejű adatot használ, két opciód van:
    • Becsomagolod <Suspense>-be → Streaming: a fallback UI kerül a statikus héjba, a tényleges tartalom kérésidőben érkezik.
    • Ellátod a "use cache" direktívával → A cache-elt eredmény bekerül a statikus héjba.
  3. Kiszolgálás — A felhasználó azonnal megkapja a statikus héjat, a dinamikus részek pedig stream-elődnek, ahogy elkészülnek.

Ha egyik módszert sem alkalmazod egy dinamikus komponensre, a Next.js build időben hibát dob: „Uncached data was accessed outside of <Suspense>". Ez szándékos — a rendszer rákényszerít, hogy explicit döntést hozz. Személy szerint szeretem ezt a megközelítést, mert végre nem kell találgatni.

A "use cache" direktíva részletesen

Alapvető használat

A "use cache" direktíva aszinkron függvények és komponensek visszatérési értékét cache-eli. Három szinten alkalmazhatod:

1. Fájl szinten — az egész fájl cache-elődik:

// app/products/page.tsx
'use cache';

import { cacheLife } from 'next/cache';

export default async function ProductsPage() {
  cacheLife('hours');

  const products = await db.query('SELECT * FROM products');

  return (
    <div>
      <h1>Termékeink</h1>
      <ul>
        {products.map(p => (
          <li key={p.id}>{p.name} — {p.price} Ft</li>
        ))}
      </ul>
    </div>
  );
}

2. Komponens szinten — egyedi komponens cache-elése:

// components/BlogPosts.tsx
import { cacheLife } from 'next/cache';

export default async function BlogPosts() {
  'use cache';
  cacheLife('days');

  const posts = await fetch('https://api.example.com/posts');
  const data = await posts.json();

  return (
    <section>
      <h2>Legfrissebb bejegyzések</h2>
      {data.map((post: any) => (
        <article key={post.id}>
          <h3>{post.title}</h3>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </section>
  );
}

3. Függvény szinten — adatlekérdező függvény cache-elése:

// lib/data.ts
import { cacheLife, cacheTag } from 'next/cache';

export async function getProducts(categoryId: string) {
  'use cache';
  cacheLife('hours');
  cacheTag('products', `category-${categoryId}`);

  const products = await db
    .select()
    .from(productsTable)
    .where(eq(productsTable.categoryId, categoryId));

  return products;
}

A legtöbb projektben a 3. opciót fogod a leggyakrabban használni — adatlekérdező függvényekre rakni a cache-t a legtisztább megoldás.

Automatikus cache kulcsok

Az egyik legelegánsabb része az egésznek, hogy a cache kulcsokat automatikusan generálja a fordító. Az argumentumok és a szülő hatókörből bezárt (closure) értékek mind részévé válnak a cache kulcsnak. Szóval különböző bemenetekhez automatikusan különböző cache bejegyzések tartoznak — személyre szabott vagy paraméterezett cache-elt tartalmat is készíthetsz anélkül, hogy manuálisan kellene kulcsokat kezelned.

// A categoryId automatikusan a cache kulcs része lesz
export async function getProducts(categoryId: string) {
  'use cache';
  // categoryId = 'electronics' → külön cache bejegyzés
  // categoryId = 'clothing' → külön cache bejegyzés
  const products = await fetchProducts(categoryId);
  return products;
}

A cacheLife API: Mennyi ideig éljen a gyorsítótár?

Beépített profilok

A cacheLife függvény meghatározza, mennyi ideig maradjon érvényes a cache-elt tartalom. A Next.js 16 számos beépített profilt kínál, és ezek lefedik a legtöbb valós use case-t:

import { cacheLife } from 'next/cache';

// Rövid élettartam — árak, készletadatok
cacheLife('minutes');

// Közepes élettartam — napi néhányszor frissülő tartalom
cacheLife('hours');

// Hosszú élettartam — blogbejegyzések, stabil tartalom
cacheLife('days');

// Nagyon hosszú élettartam — ritkán változó tartalom
cacheLife('weeks');

// Maximális élettartam — háttérben revalidálódik
cacheLife('max');

Egyedi konfiguráció

Ha a beépített profilok nem felelnek meg az igényeidnek (és előbb-utóbb biztos lesz ilyen eset), egyedi konfigurációt is megadhatsz három időzítési paraméterrel:

export default async function DashboardStats() {
  'use cache';
  cacheLife({
    stale: 3600,      // 1 óra — ennyi ideig használja a kliens ellenőrzés nélkül
    revalidate: 7200,  // 2 óra — ezután háttérben frissül
    expire: 86400,     // 1 nap — ezután teljesen lejár
  });

  const stats = await db.query('SELECT * FROM dashboard_stats');
  return <StatsGrid stats={stats} />;
}

A három paraméter együttműködése röviden:

  • stale — Ennyi ideig a kliens a helyi cache-ből szolgálja ki a tartalmat, szerver felé való ellenőrzés nélkül.
  • revalidate — Ennyi idő elteltével a következő kérés háttérben frissítést indít (ez a jól ismert stale-while-revalidate szemantika).
  • expire — Ennyi idő elteltével a cache bejegyzés teljesen törlődik, mintha sosem létezett volna.

Feltételes cache élettartam

Az egyik leggyakoribb (és szerintem leghasznosabb) minta a feltételes cache élettartam — például vázlat tartalom rövidebb, publikált tartalom hosszabb ideig cache-elődik:

import { cacheLife, cacheTag } from 'next/cache';

async function getPostContent(slug: string) {
  'use cache';
  const post = await fetchPost(slug);
  cacheTag(`post-${slug}`);

  if (!post) {
    // Még nem létező tartalom — rövid cache az adatbázis terhelés csökkentésére
    cacheLife('minutes');
    return null;
  }

  if (post.status === 'draft') {
    // Vázlat — gyakrabban frissül
    cacheLife('minutes');
    return post.data;
  }

  // Publikált tartalom — hosszabb cache
  cacheLife('days');
  return post.data;
}

Partial Prerendering (PPR): Statikus és dinamikus tartalom egy oldalon

A probléma, amit a PPR megold

Képzelj el egy termékoldalt. Az elrendezés, a képek és a leírás alig változik, de a készletszám, az ajánlások és a felhasználó-specifikus műveletek igen. A Next.js 16 előtt gyakorlatilag két választásod volt:

  1. Az egész oldal statikus → gyors, de a dinamikus részek nem frissülnek.
  2. Az egész oldal dinamikus → friss adatok, de lassabb betöltés minden egyes kérésnél.

A PPR megszünteti ezt a dilemmát. A Next.js előre rendereli a statikus héjat (navigáció, elrendezés, cache-elt tartalom), a dinamikus részeket pedig <Suspense> boundary-kkel jelöli meg, amelyek kérésidőben stream-elődnek. Végre nem kell választanod — megkaphatod mindkettőt.

Teljes példa: Statikus, cache-elt és dinamikus tartalom együtt

Lássuk a gyakorlatban, hogyan néz ki, amikor a három réteg együttműködik:

// app/shop/page.tsx
import { Suspense } from 'react';
import { cookies } from 'next/headers';
import { cacheLife, cacheTag } from 'next/cache';
import Link from 'next/link';

export default function ShopPage() {
  return (
    <>
      {/* Statikus tartalom — automatikusan a héj része */}
      <header>
        <h1>Webshop</h1>
        <nav>
          <Link href="/">Főoldal</Link>
          <Link href="/kategoriak">Kategóriák</Link>
        </nav>
      </header>

      {/* Cache-elt dinamikus tartalom — a héjba kerül */}
      <ProductCatalog />

      {/* Futásidejű dinamikus tartalom — stream-elődik */}
      <Suspense fallback={<p>Kosár betöltése...</p>}>
        <UserCart />
      </Suspense>
    </>
  );
}

// Mindenki ugyanazt a termékkatalógust látja (óránként frissül)
async function ProductCatalog() {
  'use cache';
  cacheLife('hours');
  cacheTag('products');

  const products = await db.query('SELECT * FROM products WHERE active = true');

  return (
    <section>
      <h2>Termékeink</h2>
      <div className="grid grid-cols-3 gap-4">
        {products.map((product: any) => (
          <div key={product.id} className="product-card">
            <h3>{product.name}</h3>
            <p>{product.price} Ft</p>
          </div>
        ))}
      </div>
    </section>
  );
}

// Személyre szabott kosár — felhasználónként más
async function UserCart() {
  const sessionId = (await cookies()).get('session')?.value;
  if (!sessionId) return <p>Jelentkezz be a kosár megtekintéséhez.</p>;

  const cart = await fetchCart(sessionId);

  return (
    <aside>
      <h2>Kosarad ({cart.items.length} tétel)</h2>
      <p>Összesen: {cart.total} Ft</p>
    </aside>
  );
}

Ebben a példában három réteg működik együtt, és pont ez a PPR lényege:

  • A fejléc és navigáció tisztán statikus — build időben renderelődik, villámgyors.
  • A termékkatalógus cache-elődik "use cache"-sel, óránként frissül, és szintén a statikus héj része.
  • A felhasználói kosár <Suspense>-be van csomagolva — kérésidőben stream-elődik, mert cookie-t olvas.

A cacheTag és a revalidálási stratégiák

Cache bejegyzések címkézése

A cacheTag függvénnyel címkéket rendelhetsz a cache bejegyzéseidhez, amelyeket később szelektíven érvényteleníthetsz. Egy bejegyzéshez legfeljebb 128 címkét adhatsz, és minden címke maximum 256 karakter hosszú lehet. (A gyakorlatban persze ennél jóval kevesebb is bőven elég.)

import { cacheTag } from 'next/cache';

export async function getProduct(productId: string) {
  'use cache';
  cacheTag('products', `product-${productId}`);

  const product = await db.query(
    'SELECT * FROM products WHERE id = ?',
    [productId]
  );

  return product;
}

updateTag — Azonnali frissítés Server Actions-ben

Az updateTag egy vadonatúj, kifejezetten Server Actions számára tervezett API, amely read-your-writes szemantikát biztosít. Magyarul: a felhasználó azonnal látja a saját változtatásait — nem kell várnia a háttérben futó revalidálásra.

'use server';

import { updateTag } from 'next/cache';

export async function updateProductPrice(productId: string, newPrice: number) {
  // Adatbázis frissítése
  await db.query(
    'UPDATE products SET price = ? WHERE id = ?',
    [newPrice, productId]
  );

  // Cache azonnali érvénytelenítése és frissítése
  updateTag(`product-${productId}`);
  updateTag('products');
}

Az updateTag és a Server Actions tökéletesen kiegészítik egymást: a Server Action végzi a mutációt, az updateTag pedig biztosítja, hogy a UI azonnal tükrözze a változást. Ha olvastad a Server Actions-ről szóló korábbi cikkünket, ez a hiányzó puzzle-darab.

revalidateTag — Háttérben futó frissítés

A revalidateTag a Next.js 16-ban frissített aláírást kapott. Mostantól kötelező egy cacheLife profil második argumentumként, ami a stale-while-revalidate (SWR) viselkedést szabályozza:

import { revalidateTag } from 'next/cache';

// ✅ Ajánlott — SWR viselkedéssel
revalidateTag('blog-posts', 'max');
revalidateTag('news-feed', 'hours');
revalidateTag('analytics', 'days');

// Egyedi lejárati idővel
revalidateTag('products', { expire: 3600 });

// ⚠️ Elavult — egyargumentumos forma
revalidateTag('blog-posts'); // Ne használd!

Szóval mikor melyiket válaszd?

  • updateTag — Ha a felhasználónak azonnal látnia kell a változást. Tipikus példák: kosár frissítése, profil módosítása, űrlap beküldése.
  • revalidateTag — Ha az esetleges konzisztencia (eventual consistency) elfogadható. Tipikus példák: CMS tartalom frissítése, globális beállítások változása.

Mikor használj "use cache"-t és mikor Suspense-t?

Ez az egyik leggyakrabban felmerülő kérdés a Cache Components kapcsán — és jogosan. Nézzük a döntési szempontokat:

Döntési útmutató

  • "use cache" → Az adat nem függ a kérés kontextusától (cookie-k, fejlécek), és teljesen rendben van, ha több kérés ugyanazt a cache-elt értéket kapja. Tipikus példák: blogbejegyzések, terméklista, statisztikák, navigáció.
  • <Suspense> → Az adat kérésenként változik, mert felhasználó-specifikus vagy valós idejű. Tipikus példák: személyre szabott tartalom, kosár, értesítések, valós idejű feed.
  • Mindkettő együtt → Az oldalon vannak cache-elhető és dinamikus részek is. Ez a PPR igazi ereje — és őszintén szólva a leggyakoribb minta bármilyen valódi alkalmazásban.

Fontos szabály: futásidejű adatok és "use cache" nem keverhető

A cookies(), headers() és searchParams nem használhatók "use cache" hatókörön belül, mert ezek kérés-specifikus adatok. Viszont van egy elegáns megoldás: kiolvasod az értékeket egy nem cache-elt komponensben, és argumentumként átadod a cache-elt függvénynek.

import { cookies } from 'next/headers';
import { Suspense } from 'react';

export default function ProfilePage() {
  return (
    <Suspense fallback={<div>Betöltés...</div>}>
      <ProfileContent />
    </Suspense>
  );
}

// Ez a komponens NEM cache-elt — futásidejű adatot olvas
async function ProfileContent() {
  const session = (await cookies()).get('session')?.value;
  // A session értékét átadjuk a cache-elt komponensnek
  return <CachedUserData sessionId={session} />;
}

// Ez a komponens CACHE-ELT — a sessionId a cache kulcs része
async function CachedUserData({ sessionId }: { sessionId: string }) {
  'use cache';
  const data = await fetchUserData(sessionId);
  return <div>{data.name} profilja</div>;
}

Migráció korábbi verziókról: Amit tudnod kell

Eltávolított route segment konfigurációk

A Cache Components engedélyezésével több korábbi route segment konfiguráció feleslegessé válik. Íme a legfontosabb megfeleltetések:

  • dynamic = "force-dynamic" → Nincs rá szükség. Minden oldal alapból dinamikus.
  • dynamic = "force-static" → Használj "use cache"-t cacheLife('max')-szal helyette.
  • revalidate = 3600 → Cseréld le cacheLife('hours')-ra.
  • fetchCache = "force-cache" → Felesleges. A "use cache" hatókörön belül minden fetch automatikusan cache-elődik.

A middleware.ts → proxy.ts átnevezés

Bár ez nem közvetlenül a Cache Components része, érdemes tudni róla: a Next.js 16-ban a middleware.ts fájlt proxy.ts-re nevezték át. A logika változatlan marad, de a middleware.ts már elavultnak számít. Ha olvastad a middleware biztonságáról szóló korábbi cikkünket, a benne leírt elvek továbbra is érvényesek — csak a fájlnevet és az exportált függvény nevét kell frissítened.

Navigáció és az Activity komponens

A cacheComponents flag engedélyezésével a Next.js a React <Activity> komponensét használja kliens oldali navigáció során. Ez azt jelenti, hogy az útvonalak közötti navigáláskor a komponens állapot megőrződik — ha visszanavigálsz egy korábban meglátogatott oldalra, az pontosan úgy jelenik meg, ahogy elhagytad. Űrlap kitöltések, kihajtott szekciók, scroll pozíció — minden megmarad. Nagyon kellemes fejlesztés.

Teljesítmény és jó gyakorlatok

A cache-elés arany szabályai

  1. Cache-elj a lehető legközelebb az adatforráshoz — Ne az egész oldalt cache-eld, ha csak egy adatlekérdezést kellene. A komponens vagy függvény szintű cache-elés sokkal finomabb kontrollt ad.
  2. Használj címkéket a CMS tartalmakhoz — Ha tartalomkezelő rendszered van, használj hosszú élettartamú cache-t cacheTag-ekkel kombinálva, és revalidateTag-gel frissíts, amikor a tartalom ténylegesen változik.
  3. A Suspense boundary-ket helyezd minél közelebb a dinamikus részhez — Minél kisebb a Suspense boundary hatóköre, annál több tartalom kerülhet a statikus héjba. Ez közvetlen hatással van a betöltési sebességre.
  4. Ne cache-elj futásidejű adatot — A cookie-k, fejlécek és keresési paraméterek mindig <Suspense>-be tartoznak, nem "use cache"-be.
  5. Egy cacheLife hívás futásonként — Ha feltételes logikád van (if/else), minden ágban meghívhatod, de futásonként mindig csak egy hajtódik végre.

Fontos korlátozás: Edge Runtime nem támogatott

A Cache Components a Node.js futtatókörnyezetet igényli. Ha Edge Runtime-ot próbálsz használni Cache Components-szel, hibát fogsz kapni. Ezt érdemes fejben tartani, különösen ha korábban Edge middleware-t használtál — a proxy.ts már Node.js runtime-on fut.

Gyakran Ismételt Kérdések (GYIK)

Mi történt az experimental.ppr flaggel a Next.js 16-ban?

A Next.js 16 eltávolította az experimental.ppr flaget és a route szintű experimental_ppr exportot. Helyettük a cacheComponents: true konfiguráció engedélyezi mind a Cache Components-et, mind a Partial Prerendering-et. Egyszerűbb, egységesebb — én is ezt hiányoltam a korábbi verziókból.

Mi a különbség az updateTag és a revalidateTag között?

Az updateTag kizárólag Server Actions-ben használható, és azonnal érvényteleníti a cache-t — a felhasználó rögtön látja a friss adatot (read-your-writes szemantika). A revalidateTag bárhol használható, és stale-while-revalidate viselkedést biztosít: a régi tartalom kiszolgálódik, miközben a háttérben megtörténik a frissítés. Interaktív műveleteknél (kosár, profil) az updateTag-et, háttérben frissülő tartalmaknál a revalidateTag-et válaszd.

Használhatok "use cache"-t cookie-kal vagy fejlécekkel együtt?

Közvetlenül nem — a cookies(), headers() és searchParams futásidejű adatok, amelyek nem cache-elhetők. De van megoldás: kiolvashatod az értékeket egy nem cache-elt komponensben, és argumentumként átadhatod egy "use cache" direktívával ellátott függvénynek. Az argumentum automatikusan a cache kulcs részévé válik, szóval a végeredmény pontosan az, amire szükséged van.

Hogyan működik a Cache Components a React Compiler-rel?

A React Compiler és a Cache Components egymástól függetlenül, de kiegészítő módon működnek. A React Compiler automatikusan memorizálja a komponenseket a felesleges újrarenderelés elkerülése érdekében (ez kliens oldalon történik), míg a Cache Components a szerver oldali adatlekérdezés és renderelés eredményét cache-eli. Mindkettő engedélyezhető a next.config.ts-ben, és az együttes használatuk komolyan megdobja a teljesítményt.

Muszáj átállnom Next.js 16-ra a Cache Components használatához?

Igen. A Cache Components és a "use cache" direktíva kizárólag a Next.js 16-ban érhető el. A korábbi verziók experimental.ppr és dynamicIO flagjei nem egyenértékűek — azok előzetes, kísérleti implementációk voltak. A stabil, végleges API a Next.js 16-ban debütált.

A Szerzőről Priya Raghavan

Priya spent four years on Vercel's developer experience team, where she wrote a chunk of the Turbopack incremental cache layer and rewrote the official App Router migration codemods used by tens of thousands of teams. Before that, she shipped the checkout funnel rewrite at Shopify on Next.js 12, cutting LCP from 3.4s to 1.1s on mid-tier Android. These days she runs an independent consultancy helping Series B companies untangle Pages Router monoliths into RSC-first App Router architectures. She has opinions about Server Actions, mostly negative ones, and is slowly coming around on Partial Prerendering after benchmarking it against six production workloads. She writes about cache semantics, streaming SSR, and the parts of the React docs that quietly contradict the Next.js docs. Eight years in frontend, three of them spent yelling at hydration mismatches.