Cache Components v Next.js 16: Průvodce direktivou use cache a Partial Prerendering

Praktický průvodce Cache Components v Next.js 16 – direktiva use cache, konfigurace cacheLife, tagování a invalidace cache, Partial Prerendering a kompletní příklady pro reálné aplikace.

Úvod

Next.js 16 přináší jednu z nejzásadnějších změn v historii tohoto frameworku – Cache Components a direktivu "use cache". Pokud jste někdy zápasili s nepředvídatelným chováním cache v předchozích verzích Next.js (a ruku na srdce, kdo ne?), kde framework sám rozhodoval, co se bude cachovat a co ne, tahle novinka vás potěší. Next.js 16 opouští model implicitního cachování a přechází na explicitní kontrolu – vy jako vývojář přesně určujete, co se má cachovat, jak dlouho a za jakých podmínek se má cache invalidovat.

V předchozích verzích bylo cachování takřka magické. Stačilo použít fetch() na serveru a odpověď se automaticky cachovala. Segmenty stránek byly implicitně statické, pokud jste nepoužili dynamické API jako cookies() nebo headers(). Pohodlné? Bezpochyby. Jenže to vedlo k řadě nečekaných situací – stránky zobrazovaly zastaralá data, vývojáři často nechápali, proč se obsah neaktualizuje, a ladění cache bylo noční můrou. Osobně jsem strávil nespočet hodin hledáním, proč se mi na stránce zobrazují data stará tři hodiny.

Cache Components tento problém řeší. Místo spoléhání na to, že framework správně odhadne vaše záměry, jednoduše přidáte direktivu "use cache" tam, kde chcete cachovat. Vše ostatní je ve výchozím stavu dynamické. A to je ta zásadní věc. Když se podíváte na kód aplikace, okamžitě vidíte, které části jsou cachované a které dynamické. Žádné studování interní logiky frameworku.

Tato změna má také významný dopad na výkon. Díky Cache Components dosáhnete rychlosti statických stránek pro cachovaný obsah, zatímco dynamické části se streamují paralelně pomocí React Suspense. Výsledkem je architektura, kde každá stránka může být částečně statická a částečně dynamická – bez kompromisů.

Takže pojďme na to. Projdeme si vše od základní aktivace, přes syntaxi a varianty direktivy, až po pokročilé vzory pro revalidaci a migraci z předchozích verzí. Ukážeme si konkrétní příklady kódu, vysvětlíme generování klíčů cache a postavíme kompletní příklad blogové aplikace.

Co jsou Cache Components a Partial Prerendering (PPR)

Cache Components jsou součástí širšího konceptu nazývaného Partial Prerendering (PPR). Tato architektura byla poprvé představena koncem roku 2023 jako experimentální funkce a v Next.js 16 je konečně připravena k produkčnímu nasazení.

Princip PPR funguje takhle: při požadavku na stránku server okamžitě odešle statický HTML shell obsahující veškerý předem vykreslený a cachovaný obsah. Dynamické sekce stránky jsou obaleny komponentou <Suspense> a jejich obsah se streamuje paralelně, jakmile je k dispozici. Uživatel tedy okamžitě vidí layout stránky, navigaci, cachovaný obsah (třeba seznam článků) a na místech dynamického obsahu (třeba personalizované doporučení) se zobrazí fallback, který je nahrazen skutečným obsahem, jakmile ho server připraví.

Klíčová myšlenka: jedna stránka může obsahovat jak statické, tak dynamické části. Bez nutnosti celou stránku renderovat buď čistě staticky, nebo čistě dynamicky. To je opravdu zásadní posun oproti předchozímu modelu, kde se celý segment musel rozhodnout pro jednu strategii.

Cache Components jsou mechanismem, který umožňuje označit konkrétní komponenty nebo funkce jako cachované. Když Next.js narazí na direktivu "use cache", ví, že výstup této komponenty má být součástí statického shellu.

Jak PPR funguje v praxi

Představte si e-commerce stránku s produktem. Hlavička, navigace a popis produktu se nemění často – ideální kandidáti pro cache. Na druhé straně, dostupnost na skladě, cena s personalizovanou slevou a obsah nákupního košíku jsou dynamické a měly by se načítat v reálném čase.

S PPR a Cache Components server okamžitě odešle statický shell s cachovaným popisem produktu a na místech dynamického obsahu zobrazí loading stavy. Dynamické sekce se pak dostreamují paralelně – košík, cena i dostupnost se načtou nezávisle na sobě.

Z hlediska uživatelského zážitku je to transformativní. Namísto zírání na prázdnou stránku uživatel okamžitě vidí strukturu stránky, navigaci a hlavní obsah. Metriky jako TTFB a FCP se výrazně zlepší, protože server může odeslat první bajty odpovědi prakticky okamžitě z cache. Dynamické části se pak postupně doplňují.

PPR není jen optimalizace – je to fundamentálně jiný přístup k renderování. Tradiční model vyžadoval binární rozhodnutí: buď je stránka statická, nebo dynamická. PPR tuto dichotomii odstraňuje a umožňuje míchání obou strategií na úrovni jednotlivých komponent. A upřímně řečeno, přesně to jsme roky potřebovali.

Aktivace Cache Components

Cache Components je nutné explicitně aktivovat v konfiguračním souboru next.config.ts. Ve výchozím stavu jsou vypnuty.

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

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

export default nextConfig;

Po aktivaci se změní výchozí chování celé aplikace:

  • Nic není automaticky cachováno – veškerý obsah je ve výchozím stavu dynamický.
  • Cachování je explicitní – pouze komponenty a funkce s direktivou "use cache" jsou cachovány.
  • PPR je automaticky aktivováno – stránky mohou obsahovat jak statické, tak dynamické části.
  • Suspense boundary se stávají hranicemi mezi statickým a dynamickým obsahem.

Důležité upozornění: aktivace cacheComponents je jednosměrná změna. Nelze ji snadno kombinovat se starým modelem cachování. Pokud ji zapnete, měli byste migrovat celou aplikaci najednou. Starší volby jako dynamic, revalidate a fetchCache přestávají fungovat a jsou nahrazeny direktivou "use cache" a souvisejícími API.

Doporučuji aktivovat Cache Components na novém projektu nebo při větší refaktorizaci. Postupná migrace po jednotlivých stránkách není podporována – jedná se o globální změnu. Více o migraci najdete dále v článku.

Jak funguje renderování s Cache Components

Po aktivaci Cache Components Next.js rozděluje obsah stránky do tří kategorií:

1. Automaticky předrenderovaný obsah

Některý obsah Next.js automaticky zahrne do statického shellu, aniž by vyžadoval direktivu "use cache". Jedná se o obsah, který je čistě synchronní a deterministický:

  • Synchronní I/O operace – import modulů, čtení konstant
  • Importy komponent – statický strom komponent bez asynchronních závislostí
  • Čisté výpočty – výpočty závisející pouze na vstupních parametrech
// Tato komponenta je automaticky součástí statického shellu
function Footer() {
  const year = new Date().getFullYear(); // deterministické při buildu
  return (
    <footer>
      <p>&copy; {year} Moje aplikace</p>
    </footer>
  );
}

2. Dynamický obsah odložený pomocí Suspense

Obsah závisející na runtime datech je automaticky odložen a streamován:

  • Síťové požadavky – volání API, fetch požadavky bez cache
  • Databázové dotazy – přímé dotazy na databázi
  • Dynamická APIcookies(), headers(), searchParams
import { Suspense } from 'react';

// Dynamická komponenta – bude streamována
async function UserGreeting() {
  const user = await getCurrentUser(); // závisí na cookies
  return <p>Ahoj, {user.name}!</p>;
}

// Stránka s PPR
export default function Page() {
  return (
    <div>
      <h1>Vítejte</h1> {/* statický shell */}
      <Suspense fallback={<p>Načítám...</p>}>
        <UserGreeting /> {/* streamováno */}
      </Suspense>
    </div>
  );
}

3. Cachovaný obsah pomocí „use cache"

Obsah označený direktivou "use cache" je zahrnut do statického shellu a cachován. Ideální pro obsah, který se sice načítá z externích zdrojů, ale nemění se příliš často:

async function FeaturedProducts() {
  "use cache";
  const products = await db.products.findMany({
    where: { featured: true },
    take: 10,
  });
  return (
    <section>
      {products.map((product) => (
        <ProductCard key={product.id} product={product} />
      ))}
    </section>
  );
}

Tato komponenta provede databázový dotaz pouze jednou (nebo po vypršení cache) a její HTML výstup se uloží. Při dalších požadavcích se vrátí cachovaný výstup bez opětovného dotazu na databázi. Rychlejší odezva pro uživatele a nižší zatížení databáze – win-win.

Výběr správné kategorie pro každý kus obsahu je klíčové rozhodnutí. Obecně – čím méně se obsah mění a čím nákladnější je jeho generování, tím větší smysl dává cachování. Obsah závislý na identitě uživatele by měl zůstat dynamický. A ten automaticky předrenderovaný obsah? Ten prostě funguje sám, žádná konfigurace potřeba.

Direktiva „use cache" v detailu

Direktiva "use cache" se zapisuje jako řetězcový literál na začátku funkce, komponenty nebo souboru – podobně jako "use client" nebo "use server". Pokud znáte tyto direktivy, syntaxe vám bude okamžitě povědomá. Direktiva musí být úplně první výraz v daném scope – před ní nesmí být žádný jiný kód kromě komentářů.

Lze ji použít na třech úrovních:

Úroveň souboru

Když umístíte "use cache" na začátek souboru, všechny exportované funkce a komponenty budou automaticky cachovány:

"use cache";

// Všechny exporty z tohoto souboru jsou cachovány

export async function getCategories() {
  const categories = await db.categories.findMany();
  return categories;
}

export async function getPopularProducts() {
  const products = await db.products.findMany({
    orderBy: { sales: 'desc' },
    take: 20,
  });
  return products;
}

export default function CatalogSidebar() {
  // i tato komponenta je cachována
  return (
    <aside>
      {/* ... */}
    </aside>
  );
}

Úroveň komponenty

Direktivu můžete umístit přímo do těla komponenty – cachujete tak pouze tu konkrétní:

async function BlogPostList({ category }: { category: string }) {
  "use cache";
  const posts = await db.posts.findMany({
    where: { category, published: true },
    orderBy: { createdAt: 'desc' },
  });
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <a href={`/blog/${post.slug}`}>{post.title}</a>
        </li>
      ))}
    </ul>
  );
}

Všimněte si, že komponenta přijímá category jako prop. Ten se stává součástí klíče cache – pro každou unikátní hodnotu category se vytvoří samostatná položka v cache.

Úroveň funkce

Direktivu lze použít i na běžné asynchronní funkce. To se hodí pro sdílenou logiku načítání dat:

async function getProductById(id: string) {
  "use cache";
  const product = await db.products.findUnique({
    where: { id },
    include: { reviews: true, category: true },
  });
  return product;
}

// Použití v komponentě
export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const product = await getProductById(id);

  if (!product) return <NotFound />;

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  );
}

Serializace argumentů

Všechny argumenty předávané do funkce nebo komponenty s "use cache" musí být serializovatelné. Next.js je používá k vytvoření klíče cache, takže musí být schopen je převést na stabilní řetězcovou reprezentaci.

Podporované typy

  • Primitivní hodnotystring, number, boolean, null, undefined, bigint
  • Prosté objekty – objekty vytvořené pomocí objektového literálu {}
  • Pole – včetně vnořených polí
  • Date – instance třídy Date
  • Map a Set – instance Map a Set
  • TypedArray – jako Uint8Array, Float32Array apod.

Nepodporované typy

  • Instance tříd – vlastní třídy s prototypem nelze serializovat
  • Funkce – s výjimkou pass-through vzoru (viz další sekce)
  • SymbolySymbol() nelze serializovat
  • DOM elementy – instance jako HTMLElement
  • Streamy a iterátory

Příklady platných a neplatných argumentů

// Platné argumenty
async function getCachedData(
  id: string,                    // string - OK
  count: number,                 // number - OK
  filters: { active: boolean },  // prostý objekt - OK
  tags: string[],                // pole - OK
  since: Date,                   // Date - OK
  metadata: Map<string, string> // Map - OK
) {
  "use cache";
  // ...
}

// NEPLATNÉ argumenty - způsobí runtime chybu
class UserFilter {
  constructor(public role: string) {}
}

async function getFilteredUsers(filter: UserFilter) {
  "use cache"; // CHYBA: instance třídy není serializovatelná
  // ...
}

async function getProcessedData(transform: (data: any) => any) {
  "use cache"; // CHYBA: funkce není serializovatelná
  // ...
}

Pokud předáte neserializovatelný argument, Next.js vyhodí runtime chybu s jasným popisem problému. Framework vás tím chrání před situacemi, kdy by cache mohla fungovat nesprávně kvůli nestabilním klíčům.

V praxi to znamená, že rozhraní cachovaných funkcí a komponent je potřeba navrhovat s ohledem na serializaci. Místo předávání složitých objektů je často lepší předat jen identifikátor a nechat cachovanou funkci, aby si data načetla sama. Tenhle vzor je nejen kompatibilní se serializací, ale taky zajišťuje stabilní a prediktabilní cache klíč – a to je pro správnou invalidaci klíčové.

Pass-through vzor

Existuje důležitá výjimka z pravidla serializace: hodnoty, které procházejí skrz cachovanou komponentu, aniž by byly čteny. Říká se tomu pass-through a umožňuje předávat neserializovatelné hodnoty jako children nebo server akce.

Princip je jednoduchý. Pokud cachovaná komponenta neserializovatelnou hodnotu nečte, ale pouze ji předá dále do výstupu (třeba jako {children}), Next.js ji nepoužije jako součást klíče cache, ale zachová ji pro výsledný render.

async function CachedLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  "use cache";
  const navigation = await getNavigation();

  return (
    <div className="layout">
      <nav>
        {navigation.map((item) => (
          <a key={item.href} href={item.href}>{item.label}</a>
        ))}
      </nav>
      <main>{children}</main> {/* pass-through: children nejsou čtena */}
    </div>
  );
}

Navigace se cachuje, ale children procházejí beze změny. Každý požadavek může dodat jiný children obsah a cache navigace zůstane platná.

Pass-through pro server akce

Stejný vzor funguje pro server akce předávané jako props:

async function CachedProductCard({
  productId,
  addToCartAction,
}: {
  productId: string;
  addToCartAction: (id: string) => Promise<void>;
}) {
  "use cache";
  const product = await getProductById(productId);

  return (
    <div className="product-card">
      <h3>{product.name}</h3>
      <p>{product.price} Kč</p>
      {/* Server akce prochází skrz, není čtena v cache scope */}
      <form action={addToCartAction.bind(null, productId)}>
        <button type="submit">Přidat do košíku</button>
      </form>
    </div>
  );
}

Klíč cache se vytvoří pouze z productId. Funkce addToCartAction prochází skrz a je zahrnuta do výstupu, ale do klíče cache se nepočítá.

Pravidla pro pass-through

Pass-through vzor má několik přísných pravidel:

  • Hodnota nesmí být čtena, inspektována ani podmíněně použita uvnitř cache scope.
  • Hodnota musí být předána přímo do JSX výstupu nebo volání funkce.
  • Pokud se pokusíte přečíst pass-through hodnotu (třeba zavolat předanou funkci), dostanete chybu.
  • Nelze pass-through hodnotu uložit do proměnné a později podmíněně vyrenderovat – musí být vždy zahrnuta do výstupu.

Tenhle vzor je obzvlášť užitečný při vytváření cachovaných layout komponent obalujících dynamický obsah. Navigace, patička a boční panel se cachují, hlavní obsah (children) prochází beze změny a může být plně dynamický. Solidní úspora výpočetního času bez ztráty flexibility.

Generování klíčů cache

Pochopení toho, jak Next.js generuje klíče cache, je pro efektivní práci s Cache Components naprosto zásadní. Klíč cache je unikátní identifikátor určující, jestli se použije cachovaná verze, nebo se obsah vyrenderuje znovu. Když tohle pochopíte, vyhnete se dvěma typickým problémům: nadměrnému cachování (zbytečně duplicitní položky) a nedostatečnému cachování (různý obsah servírovaný ze stejné cache).

Klíč cache se skládá ze tří částí:

  1. Build ID – unikátní identifikátor sestavení. Při každém novém deployi se vygeneruje nový, čímž se automaticky invaliduje celá cache.
  2. Function ID – identifikátor konkrétní funkce nebo komponenty. Stabilní napříč requesty, ale mění se při změně kódu.
  3. Serializované argumenty – hodnoty všech serializovatelných argumentů.
// Příklad: různé klíče cache pro různé argumenty
async function getArticles(category: string, page: number) {
  "use cache";
  const articles = await db.articles.findMany({
    where: { category },
    skip: (page - 1) * 10,
    take: 10,
  });
  return articles;
}

// Tyto volání vytvoří různé položky v cache:
await getArticles("technologie", 1); // klíč: [buildId, funcId, "technologie", 1]
await getArticles("technologie", 2); // klíč: [buildId, funcId, "technologie", 2]
await getArticles("veda", 1);        // klíč: [buildId, funcId, "veda", 1]

Pozor na uzávěry (closures)

Tohle je past, do které se snadno chytíte. Hodnoty zachycené v uzávěře nejsou součástí klíče cache:

function createCachedFetcher(apiUrl: string) {
  // POZOR: apiUrl je zachyceno v uzávěře, není argumentem
  async function fetchData(endpoint: string) {
    "use cache";
    const response = await fetch(`${apiUrl}${endpoint}`);
    return response.json();
  }
  return fetchData;
}

// Tyto dva fetchery sdílejí stejnou cache, protože apiUrl
// není součástí klíče cache!
const fetchFromProd = createCachedFetcher("https://api.prod.example.com");
const fetchFromStaging = createCachedFetcher("https://api.staging.example.com");

// Obě volání mohou vrátit stejná cachovaná data!
await fetchFromProd("/users");
await fetchFromStaging("/users"); // Může vrátit data z prod!

Řešení? Předejte všechny proměnné ovlivňující cache jako explicitní argumenty:

async function fetchData(apiUrl: string, endpoint: string) {
  "use cache";
  const response = await fetch(`${apiUrl}${endpoint}`);
  return response.json();
}

Řízení životnosti cache pomocí cacheLife

Ve výchozím stavu má cache omezenou životnost. Next.js poskytuje funkci cacheLife pro přesné řízení toho, jak dlouho cachovaný obsah zůstane platný. Volá se uvnitř funkce označené direktivou "use cache".

Výchozí profil

Pokud nespecifikujete cacheLife, použije se výchozí profil:

  • stale: 5 minut – po tuto dobu se vrací cachovaná verze bez revalidace
  • revalidate: 15 minut – po vypršení stale periody se spustí revalidace na pozadí
  • expire: neurčeno – po vypršení se cache zcela odstraní

Přednastavené profily

Pro běžné scénáře máte k dispozici několik přednastavených profilů:

import { cacheLife } from 'next/cache';

async function getFrequentlyUpdatedData() {
  "use cache";
  cacheLife('minutes'); // stale: 5 min, revalidate: 1 min
  const data = await fetchLatestPrices();
  return data;
}

async function getHourlyData() {
  "use cache";
  cacheLife('hours'); // stale: 1 hodina, revalidate: 1 hodina
  const data = await fetchDailyStats();
  return data;
}

async function getDailyContent() {
  "use cache";
  cacheLife('days'); // stale: 1 den, revalidate: 1 den
  const content = await fetchBlogPosts();
  return content;
}

async function getStaticContent() {
  "use cache";
  cacheLife('max'); // stale: neomezeně, revalidate: nikdy
  const config = await fetchSiteConfig();
  return config;
}

Vlastní konfigurace

Když potřebujete jemnější kontrolu, předejte vlastní objekt s hodnotami stale, revalidate a expire (v sekundách):

import { cacheLife } from 'next/cache';

async function getProductCatalog() {
  "use cache";
  cacheLife({
    stale: 300,      // 5 minut – servíruj stale verzi
    revalidate: 900, // 15 minut – revaliduj na pozadí
    expire: 3600,    // 1 hodina – po této době cache expiruje úplně
  });

  const products = await db.products.findMany({
    where: { active: true },
    include: { images: true },
  });
  return products;
}

async function getExchangeRates() {
  "use cache";
  cacheLife({
    stale: 60,       // 1 minuta
    revalidate: 120, // 2 minuty
    expire: 600,     // 10 minut
  });

  const rates = await fetch('https://api.example.com/rates');
  return rates.json();
}

Tři parametry ve zkratce:

  • stale – Jak dlouho se cachovaný obsah považuje za čerstvý. Během této doby se vrací cache bez jakékoliv kontroly.
  • revalidate – Stale-while-revalidate okno. Po vypršení stale se stále vrací cache, ale na pozadí se spustí nové renderování. Jakmile je nový obsah připraven, nahradí starý.
  • expire – Maximální celková doba životnosti. Po vypršení se cache odstraní a další požadavek musí čekat na čerstvé renderování.

Tagování a revalidace: cacheTag, updateTag a revalidateTag

Časová revalidace pomocí cacheLife je fajn pro obsah, který se mění předvídatelně. Ale co když potřebujete invalidovat cache okamžitě – třeba když uživatel aktualizuje profil, správce přidá nový produkt nebo se změní důležitá konfigurace? Čekat na vypršení časového limitu prostě nejde. K tomu slouží systém tagů.

Princip je jednoduchý: každé cachované komponentě přiřadíte jeden nebo více tagů a následně můžete invalidovat vše sdílející daný tag. Tím můžete vytvářet hierarchické invalidační strategie – od jednoho konkrétního produktu až po celý katalog.

Tagování cache pomocí cacheTag

Funkce cacheTag přiřadí jednu nebo více značek:

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

async function getProduct(id: string) {
  "use cache";
  cacheTag(`product-${id}`, 'products');
  cacheLife('hours');

  const product = await db.products.findUnique({ where: { id } });
  return product;
}

async function getCartItems(userId: string) {
  "use cache";
  cacheTag(`cart-${userId}`, 'carts');
  cacheLife('minutes');

  const items = await db.cartItems.findMany({
    where: { userId },
    include: { product: true },
  });
  return items;
}

Invalidace pomocí updateTag

updateTag provede okamžitou invalidaci. Další požadavek musí čekat na čerstvé renderování – žádná stale verze:

"use server";

import { updateTag } from 'next/cache';

export async function updateProduct(id: string, data: ProductData) {
  await db.products.update({ where: { id }, data });

  // Okamžitá invalidace – další požadavek musí čekat na nový render
  updateTag(`product-${id}`);
}

export async function deleteProduct(id: string) {
  await db.products.delete({ where: { id } });

  // Invalidovat konkrétní produkt i celý seznam
  updateTag(`product-${id}`);
  updateTag('products');
}

Invalidace pomocí revalidateTag

revalidateTag provede stale-while-revalidate invalidaci. Další požadavek okamžitě vrátí starší verzi, ale na pozadí se spustí nové renderování:

"use server";

import { revalidateTag } from 'next/cache';

export async function addToCart(userId: string, productId: string) {
  await db.cartItems.create({
    data: { userId, productId, quantity: 1 },
  });

  // Stale-while-revalidate – uživatel okamžitě vidí odpověď,
  // košík se aktualizuje na pozadí
  revalidateTag(`cart-${userId}`);
}

Kdy použít updateTag vs. revalidateTag

Záleží na tom, jak moc na konzistenci dat záleží:

  • updateTag – Když uživatel očekává aktualizovaná data okamžitě. Po editaci profilu, změně hesla, smazání položky.
  • revalidateTag – Když je přijatelné krátce zobrazit starší data. Aktualizace počtu komentářů, statistik, doporučení.

Varianty direktivy use cache

Direktiva "use cache" má tři varianty určující, kde a jak se cache ukládá. Výběr závisí na tom, jestli potřebujete uživatelsky specifická data, kolik máte instancí serveru a jaké jsou vaše nároky na konzistenci.

use cache (výchozí)

Základní varianta. Cache se ukládá do in-memory LRU cache na serveru. Nejrychlejší varianta, ale cache se ztratí při restartu a nesdílí se mezi instancemi.

async function getRecentArticles() {
  "use cache";
  cacheLife('hours');
  const articles = await db.articles.findMany({
    orderBy: { createdAt: 'desc' },
    take: 10,
  });
  return articles;
}

Vhodné pro jednoinstanční nasazení, obsah snadno regenerovatelný a development. V produkci OK, pokud běžíte na jedné instanci nebo je regenerace dostatečně rychlá. LRU strategie automaticky odstraňuje nejstarší nepoužívané položky, když cache dosáhne limitu.

use cache: private

Ukládá cache na straně prohlížeče. Zásadní rozdíl – uvnitř "use cache: private" lze přistupovat k cookies() a headers(), protože cache je specifická pro daného uživatele.

async function getUserDashboard() {
  "use cache: private";
  cacheLife('minutes');

  const session = await cookies();
  const userId = session.get('userId')?.value;

  if (!userId) return null;

  const dashboard = await db.dashboards.findUnique({
    where: { userId },
    include: { widgets: true },
  });
  return dashboard;
}

Vhodné pro personalizovaný obsah, dashboardy, uživatelská nastavení. Cache žije v prohlížeči, není sdílena mezi uživateli.

use cache: remote

Cache se ukládá do vzdáleného trvalého úložiště. Přežívá restart serveru, je sdílena mezi všemi instancemi. Počítejte ale s tím, že to může znamenat dodatečné náklady na infrastrukturu.

async function getGlobalConfig() {
  "use cache: remote";
  cacheLife('days');

  const config = await db.siteConfig.findFirst();
  return config;
}

async function getPopularProducts() {
  "use cache: remote";
  cacheLife({
    stale: 3600,
    revalidate: 7200,
    expire: 86400,
  });

  const products = await db.products.findMany({
    orderBy: { viewCount: 'desc' },
    take: 50,
  });
  return products;
}

Vhodné pro multi-instanční nasazení, sdílený obsah, data nákladná na regeneraci. Na Vercelu se remote cache integruje automaticky. Při self-hosted nasazení potřebujete vlastní backend – typicky Redis nebo Memcached.

Kompletní příklad: blog s Cache Components

Dost teorie. Pojďme si ukázat realistický příklad blogové aplikace, která kombinuje všechny tři typy obsahu – statický shell, cachované články a dynamické uživatelské preference. Aplikace má cachovanou navigaci (aktualizovanou při změně kategorií), seznam nejnovějších článků (revalidovaný každých 15 minut) a personalizovaná doporučení (plně dynamická podle přihlášeného uživatele).

Konfigurace

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

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

export default nextConfig;

Layout s cachovanou navigací

// app/layout.tsx
import { Suspense } from 'react';
import { CachedNavigation } from '@/components/CachedNavigation';
import { UserMenu } from '@/components/UserMenu';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="cs">
      <body>
        <header>
          <CachedNavigation />
          <Suspense fallback={<div>Načítám menu...</div>}>
            <UserMenu />
          </Suspense>
        </header>
        <main>{children}</main>
      </body>
    </html>
  );
}

Cachovaná navigace

// components/CachedNavigation.tsx
import { cacheTag, cacheLife } from 'next/cache';

export async function CachedNavigation() {
  "use cache";
  cacheTag('navigation');
  cacheLife('days');

  const categories = await db.categories.findMany({
    where: { showInNav: true },
    orderBy: { order: 'asc' },
  });

  return (
    <nav className="main-nav">
      <a href="/">Domů</a>
      {categories.map((cat) => (
        <a key={cat.slug} href={`/kategorie/${cat.slug}`}>
          {cat.name}
        </a>
      ))}
    </nav>
  );
}

Dynamické uživatelské menu

// components/UserMenu.tsx
import { cookies } from 'next/headers';

export async function UserMenu() {
  // Žádné "use cache" – tato komponenta je plně dynamická
  const session = await cookies();
  const userId = session.get('userId')?.value;

  if (!userId) {
    return (
      <div className="user-menu">
        <a href="/prihlaseni">Přihlásit se</a>
      </div>
    );
  }

  const user = await db.users.findUnique({ where: { id: userId } });

  return (
    <div className="user-menu">
      <span>Ahoj, {user?.name}</span>
      <a href="/profil">Profil</a>
      <a href="/odhlaseni">Odhlásit se</a>
    </div>
  );
}

Stránka se seznamem článků

// app/page.tsx
import { Suspense } from 'react';
import { CachedBlogPosts } from '@/components/CachedBlogPosts';
import { PersonalizedRecommendations } from '@/components/PersonalizedRecommendations';

export default function HomePage() {
  return (
    <div>
      <section className="hero">
        <h1>Vítejte na našem blogu</h1>
        <p>Nejnovější články z oblasti technologií</p>
      </section>

      {/* Cachované – součástí statického shellu */}
      <section className="latest-posts">
        <h2>Nejnovější články</h2>
        <CachedBlogPosts />
      </section>

      {/* Dynamické – streamováno */}
      <Suspense fallback={<div>Připravuji doporučení...</div>}>
        <PersonalizedRecommendations />
      </Suspense>
    </div>
  );
}

Cachované články

// components/CachedBlogPosts.tsx
import { cacheTag, cacheLife } from 'next/cache';

export async function CachedBlogPosts() {
  "use cache";
  cacheTag('blog-posts');
  cacheLife({
    stale: 300,      // 5 minut
    revalidate: 900, // 15 minut
    expire: 3600,    // 1 hodina
  });

  const posts = await db.posts.findMany({
    where: { published: true },
    orderBy: { publishedAt: 'desc' },
    take: 10,
    include: {
      author: { select: { name: true, avatar: true } },
      tags: true,
    },
  });

  return (
    <div className="blog-grid">
      {posts.map((post) => (
        <article key={post.id} className="blog-card">
          <a href={`/blog/${post.slug}`}>
            <h3>{post.title}</h3>
            <p>{post.excerpt}</p>
            <div className="meta">
              <span>{post.author.name}</span>
              <time dateTime={post.publishedAt.toISOString()}>
                {post.publishedAt.toLocaleDateString('cs-CZ')}
              </time>
            </div>
            <div className="tags">
              {post.tags.map((tag) => (
                <span key={tag.id} className="tag">{tag.name}</span>
              ))}
            </div>
          </a>
        </article>
      ))}
    </div>
  );
}

Personalizovaná doporučení

// components/PersonalizedRecommendations.tsx
import { cookies } from 'next/headers';

export async function PersonalizedRecommendations() {
  const session = await cookies();
  const userId = session.get('userId')?.value;

  if (!userId) {
    return null;
  }

  const recommendations = await getRecommendations(userId);

  return (
    <section className="recommendations">
      <h2>Doporučeno pro vás</h2>
      <div className="recommendation-grid">
        {recommendations.map((post) => (
          <a key={post.id} href={`/blog/${post.slug}`}>
            <h4>{post.title}</h4>
            <p>{post.excerpt}</p>
          </a>
        ))}
      </div>
    </section>
  );
}

Server akce pro invalidaci

// actions/blog.ts
"use server";

import { updateTag, revalidateTag } from 'next/cache';

export async function publishPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  const post = await db.posts.create({
    data: {
      title,
      content,
      published: true,
      publishedAt: new Date(),
    },
  });

  // Okamžitá invalidace seznamu článků
  updateTag('blog-posts');

  return { success: true, slug: post.slug };
}

export async function updatePost(id: string, formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  await db.posts.update({
    where: { id },
    data: { title, content },
  });

  // Revalidace konkrétního článku i seznamu
  updateTag(`post-${id}`);
  revalidateTag('blog-posts');
}

export async function updateNavigation() {
  // Po změně navigace v admin panelu
  updateTag('navigation');
}

V tomhle příkladu vidíte, jak na jedné stránce koexistují tři typy obsahu: statický hero text je součástí shellu automaticky, cachované články se berou z cache (nebo se regenerují na pozadí) a personalizovaná doporučení se streamují dynamicky na základě cookies.

Co je na tom zajímavé, je rozdělení zodpovědností. Layout řeší globální strukturu a kombinuje cachované a dynamické části přes Suspense. Každá komponenta má jasnou cache strategii – navigace na dny, články na minuty/hodiny, uživatelské menu úplně bez cache. Server akce se starají o invalidaci po změně dat. A přidání nové cachované sekce? Prostě vytvoříte komponentu s "use cache" a správnými tagy.

Migrace ze starších konfigurací

Pokud migrujete existující Next.js aplikaci, budete muset nahradit starší konfigurační volby segmentů novými direktivami. Projděte všechny soubory stránek a layoutů, identifikujte používané volby a nahraďte je. Dobrá zpráva – většina migrací je přímočará a často vede k jednoduššímu kódu.

Tady je přehled nejčastějších migrací:

dynamic = "force-dynamic"

Tato volba zajišťovala, že segment je vždy dynamický. Po aktivaci Cache Components je tohle výchozí chování – jednoduše ji odstraňte.

// PŘED (Next.js 14/15)
export const dynamic = "force-dynamic";

export default async function Page() {
  const data = await fetchData();
  return <div>{data}</div>;
}

// PO (Next.js 16 s cacheComponents)
// Stačí odstranit konfiguraci – vše je dynamické ve výchozím stavu
export default async function Page() {
  const data = await fetchData();
  return <div>{data}</div>;
}

dynamic = "force-static"

Vynucené statické renderování. Nahraďte direktivou "use cache" s cacheLife('max'):

// PŘED
export const dynamic = "force-static";

export default async function Page() {
  const config = await fetchSiteConfig();
  return <div>{config.siteName}</div>;
}

// PO
import { cacheLife } from 'next/cache';

export default async function Page() {
  "use cache";
  cacheLife('max');
  const config = await fetchSiteConfig();
  return <div>{config.siteName}</div>;
}

revalidate = 3600

Časová revalidace se nahrazuje kombinací "use cache" a cacheLife:

// PŘED
export const revalidate = 3600; // 1 hodina

export default async function Page() {
  const articles = await fetchArticles();
  return <ArticleList articles={articles} />;
}

// PO
import { cacheLife } from 'next/cache';

export default async function Page() {
  "use cache";
  cacheLife('hours'); // nebo cacheLife({ stale: 3600, revalidate: 3600 })
  const articles = await fetchArticles();
  return <ArticleList articles={articles} />;
}

fetchCache

Volba fetchCache ovlivňovala chování cache pro všechny fetch() požadavky v segmentu. Plně nahrazena direktivou "use cache":

// PŘED
export const fetchCache = "force-cache";

export default async function Page() {
  const data = await fetch('https://api.example.com/data');
  return <div>{data}</div>;
}

// PO
export default async function Page() {
  "use cache";
  const data = await fetch('https://api.example.com/data');
  return <div>{data}</div>;
}

Důležité upozornění: Edge Runtime

Cache Components nepodporují Edge Runtime. Pokud vaše aplikace používá export const runtime = "edge", budete muset tyto segmenty migrovat na Node.js runtime, nebo je ponechat bez Cache Components. Cache infrastruktura potřebuje přístup k souborovému systému a dalším funkcím Node.js, které Edge Runtime nemá.

// TOTO NEFUNGUJE s Cache Components:
export const runtime = "edge";

export default async function Page() {
  "use cache"; // CHYBA: Edge Runtime nepodporuje "use cache"
  // ...
}

// ŘEŠENÍ: Odstraňte deklaraci Edge Runtime
// (Node.js runtime je výchozí)
export default async function Page() {
  "use cache"; // OK – Node.js runtime
  // ...
}

Omezení a best practices

Cache Components jsou mocný nástroj, ale mají svá omezení. Když je budete znát, ušetříte si hodiny frustrujícího ladění v produkci.

Klíčová omezení

Nelze přistupovat k dynamickým API uvnitř use cache

Uvnitř funkce označené "use cache" (základní varianta bez private) nemůžete volat cookies(), headers() ani číst searchParams. Tato API jsou dynamická a jejich výsledky by narušily determinismus cache.

// ŠPATNĚ – způsobí chybu
async function getPersonalizedContent() {
  "use cache";
  const session = await cookies(); // CHYBA!
  const userId = session.get('userId')?.value;
  // ...
}

// SPRÁVNĚ – přečtěte data vně a předejte jako argument
async function getCachedContent(userId: string) {
  "use cache";
  cacheTag(`user-content-${userId}`);
  const content = await db.content.findMany({ where: { userId } });
  return content;
}

// V komponentě stránky:
export default async function Page() {
  const session = await cookies();
  const userId = session.get('userId')?.value ?? 'anonymous';
  const content = await getCachedContent(userId);
  return <ContentList items={content} />;
}

Nelze použít React.cache pro předávání dat do use cache

React.cache (deduplikace na úrovni requestu) nefunguje v kombinaci s "use cache". Důvod je logický – React.cache je vázáno na konkrétní request, zatímco "use cache" uchovává data napříč requesty:

import { cache } from 'react';

// ŠPATNĚ – React.cache hodnoty nejsou dostupné uvnitř "use cache"
const getUser = cache(async () => {
  const session = await cookies();
  return db.users.findUnique({ where: { id: session.get('userId')?.value } });
});

async function CachedProfile() {
  "use cache";
  const user = await getUser(); // NEBUDE FUNGOVAT správně
  return <div>{user.name}</div>;
}

// SPRÁVNĚ – předejte data jako argumenty
async function CachedProfile({ userId }: { userId: string }) {
  "use cache";
  const user = await db.users.findUnique({ where: { id: userId } });
  return <div>{user.name}</div>;
}

Pouze Node.js runtime

Jak už bylo řečeno – Cache Components fungují výhradně s Node.js runtime. Pokud potřebujete Edge Runtime pro některé route, ty nemohou používat "use cache".

Best practices

Na základě výše uvedeného a zkušeností z praxe tady je pár osvědčených postupů:

1. Oddělujte dynamická a statická data

Strukturujte komponenty tak, aby cachované přijímaly všechna data jako serializovatelné argumenty. Dynamická data čtěte na nejvyšší úrovni a předávejte je dolů:

// app/dashboard/page.tsx
import { Suspense } from 'react';
import { cookies } from 'next/headers';
import { CachedStats } from '@/components/CachedStats';
import { DynamicActivity } from '@/components/DynamicActivity';

export default async function DashboardPage() {
  // Dynamická data – čtení na nejvyšší úrovni
  const session = await cookies();
  const userId = session.get('userId')?.value;

  if (!userId) redirect('/login');

  return (
    <div>
      {/* Cachováno – userId jako klíč cache */}
      <CachedStats userId={userId} />

      {/* Dynamické – streamováno */}
      <Suspense fallback={<ActivitySkeleton />}>
        <DynamicActivity userId={userId} />
      </Suspense>
    </div>
  );
}

2. Používejte granulární tagy

Kombinujte specifické a obecné tagy – dává vám to flexibilitu při invalidaci:

async function getProduct(id: string) {
  "use cache";
  cacheTag(`product-${id}`);   // specifický tag pro jeden produkt
  cacheTag('products');          // obecný tag pro všechny produkty
  cacheTag('catalog');           // ještě obecnější tag pro celý katalog

  const product = await db.products.findUnique({ where: { id } });
  return product;
}

// Invalidace jednoho produktu:
updateTag('product-123');

// Invalidace všech produktů (např. po hromadném importu):
updateTag('products');

// Invalidace celého katalogu (produkty, kategorie, ...):
updateTag('catalog');

3. Volte správnou variantu cache

Jednoduchý rozhodovací strom:

  • Potřebujete přistupovat k cookies/headers? Použijte "use cache: private".
  • Sdílená data, multi-instanční nasazení? Použijte "use cache: remote".
  • Sdílená data, jedna instance, levná regenerace? Použijte "use cache".

4. Nastavte přiměřenou životnost cache

Nesahejte automaticky po cacheLife('max'). Raději zvolte kratší životnost a spolehněte se na stale-while-revalidate:

// Příliš agresivní cachování – data mohou být hodiny stará
async function getNews() {
  "use cache";
  cacheLife('max'); // NEVHODNÉ pro novinky

  return await fetchLatestNews();
}

// Přiměřené cachování – čerstvost s výkonem
async function getNews() {
  "use cache";
  cacheLife({
    stale: 60,       // 1 minuta čerstvosti
    revalidate: 300, // revalidace do 5 minut
    expire: 900,     // maximální stáří 15 minut
  });

  return await fetchLatestNews();
}

5. Testujte cache chování

V dev módu (next dev) se cache chová jinak než v produkci. Pro reálné testování:

next build && next start

Ověřte si, že cachované komponenty skutečně jdou z cache. Next.js přidává do odpovědí speciální hlavičky indikující, zda obsah šel z cache, proběhla revalidace na pozadí nebo se generoval čerstvě. Při ladění výkonnostních problémů v produkci jsou tyhle informace k nezaplacení.

6. Vyhněte se nadměrné granularitě

Není nutné cachovat každou komponentu zvlášť. Cachujte na úrovni, kde to dává smysl – typicky celé sekce sdílející stejný životní cyklus dat:

// ZBYTEČNĚ granulární
async function CachedProductName({ id }: { id: string }) {
  "use cache";
  const product = await db.products.findUnique({ where: { id } });
  return <h1>{product?.name}</h1>;
}

async function CachedProductDescription({ id }: { id: string }) {
  "use cache";
  const product = await db.products.findUnique({ where: { id } });
  return <p>{product?.description}</p>;
}

// LEPŠÍ – jedna cachovaná komponenta pro celý produkt
async function CachedProductDetail({ id }: { id: string }) {
  "use cache";
  cacheTag(`product-${id}`);
  cacheLife('hours');

  const product = await db.products.findUnique({
    where: { id },
    include: { images: true, category: true },
  });

  if (!product) return null;

  return (
    <div className="product-detail">
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <div className="images">
        {product.images.map((img) => (
          <img key={img.id} src={img.url} alt={img.alt} />
        ))}
      </div>
    </div>
  );
}

Závěr

Cache Components v Next.js 16 jsou zásadní posun v tom, jak přistupujeme k cachování v React aplikacích. Přechod od implicitního k explicitnímu cachování vyžaduje počáteční investici – musíte se zamyslet nad tím, co a jak cachovat. Ale stojí to za to.

Explicitní kontrola znamená, že přesně víte, co se cachuje. Žádné překvapení, žádné záhadné stale data. Každá cachovaná komponenta je jasně označena direktivou "use cache", což výrazně usnadňuje ladění i code review.

Granulární cachování na úrovni komponent umožňuje jemné řízení životnosti. Navigace na dny, produkty na hodiny, kurzy měn na minuty – vše na jedné stránce. Systém tagů a on-demand revalidace přidávají další vrstvu flexibility.

Partial Prerendering kombinuje statické a dynamické renderování. Uživatel okamžitě vidí statický shell a dynamické sekce se plynule dostreamují. FCP i LCP se dramaticky zlepšují.

Tři varianty cache – in-memory, privátní a vzdálená – pokrývají všechno od jednoduchých aplikací po distribuované systémy.

Cache Components jsou budoucností cachování v Next.js ekosystému. Explicitní povaha, integrace s React Server Components a podpora PPR z nich dělají jeden z nejdůležitějších stavebních kamenů moderních webových aplikací. Direktiva "use cache" sleduje stejnou filozofii jako ostatní React direktivy – je deklarativní a umožňuje kompilátoru optimalizovat výstup.

Do budoucna se dá čekat další rozšíření. Možná integrace s edge cache na CDN úrovni, sofistikovanější cache warming nebo nástroje pro vizualizaci cache v produkci. Základy jsou dostatečně solidní.

Pokud s Next.js 16 ještě nepracujete, je dobrý čas začít plánovat migraci. Začněte s jednoduchou stránkou, vyzkoušejte si "use cache" a postupně přidávejte pokročilejší funkce. Za pár hodin budete mít solidní přehled o tom, jak to celé funguje, a budete schopni navrhnout cache architekturu pro své aplikace.

Pro další informace doporučuji prostudovat oficiální dokumentaci Next.js a sledovat blog Next.js, kde tým pravidelně publikuje aktualizace a návody.

O Autorovi Editorial Team

Our team of expert writers and editors.