Zapisz się na szkolenie TERAZ (ostatnie miejsca). Ruszamy za
00 dni
:
00 godz
:
00 min
:
00 sek
Dołącz teraz

Next.js 16: "use cache", Cache Components i koniec dławienia bazy danych

Next.js 16: "use cache", Cache Components i koniec dławienia bazy danych

W poprzednim artykule wdrożyliśmy Partial Prerendering (PPR). Dzięki niemu Twój dashboard ładuje się błyskawicznie – użytkownik widzi szkielet (Static Shell) w 50ms, a reszta treści dolatuje dynamicznie.

UX szybuje w górę. Użytkownik jest zachwycony. Ale po stronie serwera… mamy problem.

PPR rozwiązuje problem transportu danych (jak szybko wysłać HTML do przeglądarki), ale nie zmienia faktu, że przy każdym odświeżeniu strony Twój serwer musi tę treść wygenerować.

Jeśli masz 10,000 użytkowników i każdy odświeża dashboard:

  • Baza danych otrzymuje 10,000 ciężkich zapytań.
  • Zewnętrzne API jest odpytywane 10,000 razy.
  • React na serwerze 10,000 razy renderuje skomplikowane drzewo komponentów.

W Next.js 16 pojawia się nowa architektura

Rozwiązuje ona problem u źródła. Poznajcie dyrektywę “use cache”, Cache Components oraz model, w którym Partial Prerendering (PPR) staje się standardem.

Domyślnie cachedComponents są wyłączone, aby zachować kompatybilność wsteczną. Wystarczy jednak dodać jedną linię w next.config.ts, aby je włączyć:

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* config options here */
  reactCompiler: true,
  cacheComponents: true,
};

export default nextConfig;

Co zmienia włączenie tej opcji, przeanalizujmy to poniżej.

Wielki Reset: Dynamic Data, Static Shell

Przez lata w Next.js (App Router) walczyliśmy z domyślnym cachowaniem. System próbował być “Static by Default”, co często prowadziło do serwowania starych danych, jeśli nie dodaliśmy revalidate: 0.

Next.js 16 z włączeniem cacheComponents odwraca ten model, ale w sprytny sposób:

Dane są domyślnie dynamiczne: Aplikacja zakłada, że dane są świeże i pobierane na żądanie. Nie musisz już walczyć z cachem, żeby dostać aktualne informacje.

Route jest domyślnie pre-renderowany (PPR): mimo że dane są dynamiczne, Next.js podczas buildu próbuje wygenerować “Static Shell” – czyli szkielet aplikacji.

To my decydujemy, co zamrozić. Używając “use cache”, tworzymy “statyczne wyspy” w tym dynamicznym strumieniu. Daje to nam szybkość, niskie TTFB i świeżość danych.

Pułapka builda i connection()

Podczas wdrażania tej zmiany, możecie natrafić na błąd builda: Route / used Date.now() lub Route / used Math.random() which are not allowed during prerendering.

Zadacie pytanie: “Skoro dane są domyślnie dynamiczne, to dlaczego build krzyczy?”.

Odpowiedź tkwi w Prerenderingu. Podczas buildu Next.js próbuje wykonać kod strony, aby zbudować wspomniany Static Shell. Jeśli natrafi na funkcję, która zależy od czasu (Date.now) lub requestu, a nie jest ona oznaczona jako dynamiczna, proces zgłasza błąd, bo nie wie, jak to “zamrozić” w szkielecie.

Wtedy z pomocą przychodzi funkcja connection():

import { connection } from "next/server";

export async function getRealtimeData() {
  // 🛑 Stop Prerender!
  // Mówimy Nextowi: "To wymaga żywego połączenia. Czekaj na request użytkownika."
  await connection();

  return {
    timestamp: Date.now(),
    data: data,
  };
}

Scenariusz A: Data Cache (Odciążamy Bazę)

Wróćmy do naszego Dashboardu. Mamy raport przychodów, który agreguje miliony rekordów. Zapytanie trwa 2 sekundy.

Stworzyliśmy demo, aby porównać podejście klasyczne, z nowym podejściem i wykorzystaniem “use cache”.

Wersja Klasyczna (Realtime)

Bez cache’u, każde odświeżenie strony to pełny “spin” na bazie danych.

export async function getRevenueSlow(): Promise<RevenueData> {
  console.log("⚠️ DB HIT: Generowanie raportu (SLOW)...");
  await sleep(2000);

  return {
    amount: Math.floor(Math.random() * 100000),
    generatedAt: new Date().toLocaleTimeString(),
    source: "🔴 Baza Danych (Realtime)",
  };
}

Wersja Next.js 16 (“use cache”)

Wystarczy dodać dyrektywę i profil czasu życia (cacheLife).

export async function getRevenueCached(): Promise<RevenueData> {
  "use cache"; // Dyrektywa włączająca cache
  cacheLife("hours"); // Profil: dane są ważne przez godzinę
  cacheTag("revenue-report"); // Tag do inwalidacji

  console.log("DB HIT: Generowanie cache (FAST)...");
  await sleep(2000); // To opóźnienie poczuje tylko pierwszy user!

  return {
    amount: Math.floor(Math.random() * 100000),
    generatedAt: new Date().toLocaleTimeString(),
    source: "🟢 Cache Serwera (Memory)",
  };
}

W powyższym przykładzie cacheLife(“hours”) nie oznacza konkretnej liczby godzin.
To predefiniowany profil TTL zarządzany wewnętrznie przez Next.js (np. minutes, hours, days), a nie dokładny czas w sekundach. Zazwyczaj nazwa np. hours oznacza, że wartość revalidate oraz expire jest ustawiona na jedną godzinę. Wartość stale we wszystkich profilach TTL jest domyślnie ustawiona na 5 minut, wyjątkiem jest profil seconds, gdzie stale wynosi 30 sekund.

Możem jednak urozmaicić profile cacheLife, definiując własne w next.config.ts.

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  cacheComponents: true,
  cacheLife: {
    kursyWalut: {
      stale: 3600, // 1 godzina
      revalidate: 900, // 15 minut
      expire: 86400, // 1 dzień
    },
  },
};

export default nextConfig;
  • stale – jak długo cache jest uważany za świeży (TTL).
  • revalidate – po jakim czasie Next.js sprawdzi w tle, czy dane się zmieniły.
  • expire – maksymalny czas życia cache, po którym dane są usuwane.

Dzięki takiej konfiguracji możemy później użyć cacheLife(“kursyWalut”), w funkcji fetchującej dane kursów walut.

import { cacheLife } from "next/cache";

export async function getCachedKursyWalut() {
  "use cache";
  cacheLife("kursyWalut");
  const data = await fetch("/api/data");
  return data;
}

Wynik Side-by-Side

Uruchomiliśmy to w naszym środowisku demo. Wyniki mówią same za siebie:

Bez Cache: Czas odpowiedzi: 2040ms. Obciążenie CPU: Wysokie.

Z Cache (kolejne wejście): Czas odpowiedzi: <1ms. Obciążenie CPU: Zerowe.

Co ważne – dotyczy to też zewnętrznych API. Jeśli używasz “use cache” na funkcji fetchującej zewnętrzne dane, Twój serwer wyśle request do dostawcy API tylko raz na minutę, nawet jeśli na stronę wejdzie 1000 osób. Oszczędzasz realne pieniądze.

Scenariusz B: Włączenie Cache Components

To jest funkcjonalność, która zmienia architekturę aplikacji. Często zapominamy, że renderowanie komponentów Reacta też kosztuje.

Wyobraź sobie Sidebar. Pobiera on listę kategorii z bazy, a następnie React generuje z tego spore drzewo HTML. Nawet jeśli zcache’ujesz zapytanie do bazy (Data Cache), React nadal musi “przemielić” ten komponent przy każdym requeście.

Po co renderować Sidebar co chwilę, skoro zmienia się raz na tydzień?

Dzięki włączeniu cacheComponents: true w next.config.ts, możemy zcache’ować wynik renderowania całego komponentu.

import { cacheLife } from "next/cache";

export async function Sidebar() {
  "use cache"; // Cache'ujemy cały wyrenderowany HTML!
  cacheLife("days");

  const links = await db.navigation.findMany();

  return (
    <nav className="sidebar">{/_ ... skomplikowana struktura HTML ... _/}</nav>
  );
}

To podejście idealnie dopełnia PPR.

Strona główna jest Dynamiczna (sprawdza sesję użytkownika, cookies()).

Wewnątrz niej mamy Statyczne Wyspy (Sidebar, Footer, TopStats) – to są Cache Components.

Next.js bierze te gotowe klocki z pamięci i wstrzykuje je natychmiast, a resztę renderuje w tle.

Dowód “na żywo”: Analiza Demo

Teoria brzmi dobrze, ale jak to wygląda w praktyce? Postanowiliśmy zrobić bezlitosny test porównawczy.

Podzieliliśmy ekran na dwa niezależne okna przeglądarki:

  • Po lewej: Tradycyjna architektura (Route /uncached). Każde zapytanie uderza do bazy.
  • Po prawej: Architektura Next.js 16 (Route /cached). Dane są serwowane z pamięci.

Co widać na nagraniu?

Lewe Okno (Bez Cache): Zwróć uwagę na moment odświeżania. Za każdym razem widzisz spinner przez około 2 sekundy. Użytkownik czeka, serwer pracuje, baza danych się poci. Timestamp zmienia się przy każdym F5.

Prawe Okno (Z Cache): Tutaj sprawa wygląda inaczej. Mimo wielokrotnego odświeżania, strona ładuje się natychmiast. Nie ma spinnera, nie ma ładującego się skeletona. Serwer w ogóle nie wykonuje kodu renderowania – po prostu “wypluwa” gotowy HTML z pamięci.

Pełna Kontrola (0:18): Cache nie oznacza utraty kontroli. Pod koniec filmu klikamy przycisk “Wymuś Revalidację” w prawym oknie. Dopiero wtedy serwer odświeża dane w tle. To idealny kompromis między wydajnością statyka, a świeżością dynamicznej aplikacji. Oczywiście podobny rezultat możemy osiągnąć dobierając odpowiednie profile cacheLife, zamiast manualnej rewalidacji.

Wyniki w liczbach

MetrykaBez Cache (Tradycyjnie)Z “use cache” (Next.js 16)
Zapytania do DB (10 odświeżeń)10 zapytań1 zapytanie
Czas Odpowiedzi Serwera~2000ms (Blokujące)< 20ms (Instant)
Stabilność UISkaczące layouty (Layout Shift)Stabilny, natychmiastowy content

Jeżeli chcesz “na żywo” przestudiować opisany tu przypadek, przygotowałem demo:
Demo aplikacji: next-16-cache-components-demo.vercel.app

Oraz kod źródłowy, który łatwo przenieść do własnego projektu:
Repozytorium GitHub: JarGad23/next-16-cache-components-demo

Co z rewalidacją cache ?

Przy korzystaniu z “use cache” mamy kilka opcji rewalidacji:

  1. Automatyczna rewalidacja (cacheLife): Next.js sam sprawdza, czy dane są świeże na podstawie profilu TTL (stale, revalidate, expire).
  2. Ręczna inwalidacja (cacheTag): Możemy oznaczyć funkcję fetchującą, lub cały komponent cache tagiem i później programowo go unieważnić, np. po aktualizacji danych w bazie.

W powyższym przykładzie w demo: użyliśmy cacheTag(“revenue-report”, “kursyWalut”). To sprawi, że możemy później wywołać funkcję rewalidacji cache, w dowolnym miejscu naszej aplikacji.

"use server";
import { revalidateTag } from "next/cache";

export async function revalidateCache() {
  revalidateTag("revenue-report", "kursyWalut");
}

Pierwszy argument to nazwa taga dla którego chcemy rewalidować cache. Drugi argument (max albo inny profil z cacheLife - może być nasz customowy z next.config.ts), definiuje jaką strategię TTL zastosować przy rewalidacji. Można też przekazać obiekt z { expire: 0 }, aby wymusić natychmiastowe wygaszenie cache’u.

Różnica między revalidateTag a revalidatePath

  • revalidateTag: Markuje dane z tagami jako stale w całej aplikacji, np. gdy wiele komponentów/ścieżek używa tych samych danych.
  • revalidatePath: Unieważnia cache konkretnej ścieżki/layoutu i powoduje, że aplikacja ponownie wygeneruje tę stronę przy następnym odwiedzeniu.

Jak tworzyć własne cache tags?

cacheTag() przyjmuje dowolną liczbę tagów, ale warto pamiętać, że:

każdy argument to osobny, niezależny tag cache.

Dlatego sposób, w jaki nazwiesz tagi, ma ogromne znaczenie dla późniejszej rewalidacji.

Najlepszą praktyką jest stosowanie czytelnych, złożonych nazw tagów, zamiast przekazywania wielu luźnych argumentów.

import { cacheTag } from "next/cache";

interface UserProfile {
  id: string;
  name: string;
  email: string;
}

export async function getUserProfile(userId: string): Promise<UserProfile> {
  "use cache";

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

  // Zamiast wielu tagów, używamy jednego, złożonego tagu np. "user-profile:123" jest bardziej kontrolowalne niż dwa osobne tagi ("user-profile" i "123").

  // cacheTag("get-user-profile", user.id); - Mniej zalecane
  cacheTag(`user-profile:${user.id}`);

  return user;
}

Trik z Cache Warmup – jak uniknąć „cold startu”?

Cold start to moment, gdy cache jest pusty i pierwszy użytkownik musi poczekać na wygenerowanie danych. Aby zminimalizować to ryzyko, możemy zastosować strategię Cache Warmup.

W praktyce polega ona na tym, że:

  • serwer (np. cron job lub zewnętrzny worker)
  • wywołuje rzeczywistą trasę lub komponent, który korzysta z use cache
  • zanim zrobią to prawdziwi użytkownicy

Dzięki temu cache zostaje „rozgrzany” w naturalnym kontekście renderowania.

Mała uwaga techniczna odnośnie Cache Warmup

Cache warmup nie polega na bezpośrednim wywołaniu funkcji z use cache, ale na symulacji realnego requestu, który zapisze cache w pamięci Next.js.

To podejście:

  • poprawia UX,
  • redukuje ryzyko „pechowego” użytkownika trafiającego na rewalidację,
  • ale nie eliminuje cold startu całkowicie — to świadomy kompromis architektoniczny.

Kiedy Cache Components mają największy sens?

Cache Components najlepiej sprawdzają się tam, gdzie:

  • wszyscy użytkownicy widzą to samo
  • dane zmieniają się rzadko
  • renderowanie komponentu jest kosztowne

Przykładowe use case’y

  • Rozbudowana nawigacja / mega-menu
    • sklep E-commerce i nawigacja z tysiącami kategorii
  • Sidebar, Footer, Header
    • stopka z danymi kontaktowymi, linkami, certyfikatami
  • Listy kategorii, filtrów
    • filtry produktów w sklepie, kategorie postów na blogu
  • Content marketing
    • opisy ofert, sekcje informacyjne, FAQ
Możemy to prosto podsumować:

Jeśli renderowanie komponentu jest kosztowne i zmienia się rzadko lub wcale, to jest idealnym kandydatem do zastosowania Cache Component.

Krytyczna Analiza: Kiedy NIE używać?

“use cache” to potężne narzędzie, ale nie jest uniwersalnym rozwiązaniem na wszystko. Jako inżynierowie musimy wiedzieć, kiedy powiedzieć “nie”.

Kiedy Cache Components nie mają sensu?

Proste strony statyczne (Blog, Landing Page): Jeśli Twoja strona nie ma logowania ani personalizacji, nie komplikuj życia. Klasyczne SSG (Static Site Generation), gdzie cała strona jest budowana raz do HTML, jest prostsze i tańsze w hostingu.

Dane ultra-personalne i realtime: Przykład: „Licznik nieprzeczytanych wiadomości”. Cache’owanie takich danych po stronie serwera jest ryzykowne (nie z powodu samego use cache, ale błędnej strategii kluczy i tagów), oraz nieefektywne — cache hit ratio jest bliskie zeru.

Podsumowanie

Next.js 16 daje nam pełny zestaw narzędzi do optymalizacji

PPR = Szybki Transport (Frontend/UX).

use cache (Data) = Oszczędność Bazy Danych (Backend/Koszt).

Cache Components = Oszczędność CPU (Rendering).

Słowo na koniec

Jeżeli spodobał Ci się ten temat, sprawdź inne nasze posty po więcej konkretnych porad wydajnościowych w świecie Next.js i Reacta.

A jeśli chcesz w pełni zgłębić tematykę Web Performance – przejść przez optymalizację Core Web Vitals, zaawansowane wzorce renderowania w Next.js i wydajność samego Reacta – zachęcamy do zapisu na nasze szkolenie. Wszystkie te tematy (i wiele więcej) analizujemy dogłębnie z uczestnikami na żywo. To świetna okazja na realne podniesienie swoich kompetencji.

Sprawdź agendę: Szkolenie WDI - Web Performance City


Zobacz nasze wideo

Nie zwlekaj. Dołącz do gangu i przejdź przez misje.

To szkolenie to masa wiedzy, praktyki, ale też dobrej zabawy. Wierzymy, że edukacja może być po prostu FAJNA :-)

Dołączam i widzimy się 22-23. kwietnia 2026
WDI Web Performance City - trenerzy Bartek Miś i Jarek Gad