Hubert
14 min
9 kwietnia, 2025

Jak działa cache w Next.js 15? Praktyczne wprowadzenie z przykładami

W świecie frameworków JavaScriptowych Next.js przez lata budował reputację narzędzia łączącego elastyczność Reacta z możliwościami optymalizacji serwera. W wersji 15 twórcy Next.js dokonali przełomu w podejściu do cache – rezygnując z domyślnego agresywnego cachowania na rzecz większej kontroli po stronie programisty. Ten artykuł to przewodnik po nowym modelu cache’owania w Next.js 15. Dowiesz się, jak działa nowa dyrektywa 'use cache', czym jest dynamicIO i jak praktycznie wykorzystać te funkcje, aby Twoje aplikacje były nie tylko szybsze, ale też bardziej przewidywalne w działaniu.

Czytaj więcej
Jak działa cache w Next.js 15? Praktyczne wprowadzenie z przykładami

Dlaczego cache jest tak ważny?

Cache to technika, która pozwala aplikacji pamiętać wcześniej pobrane dane i unikać niepotrzebnych zapytań sieciowych lub kosztownych obliczeń. Dobrze zaprojektowane cache potrafi diametralnie przyspieszyć ładowanie stron i zmniejszyć obciążenie serwera. Ale cache ma też swoją ciemną stronę: przeterminowane dane, błędne rewalidacje i utrata kontroli nad tym, co kiedy się odświeża.

Next.js do wersji 14 stosował domyślnie mechanizmy, które automatycznie cache’owały dane z fetch. Problem w tym, że często prowadziło to do nieoczekiwanych rezultatów – szczególnie w dynamicznych aplikacjach. W Next.js 15 zmienia się filozofia: domyślnie cache jest wyłączony, a odpowiedzialność za jego uruchomienie spoczywa na programiście.

To podejście daje większą kontrolę, ale wymaga zrozumienia, jak działa nowy model i kiedy warto z niego korzystać.

Nowy model cache’owania w Next.js 15 – co się zmieniło?

Największą zmianą jest to, że funkcje asynchroniczne nie są już domyślnie cache’owane. W poprzednich wersjach, kiedy używałeś fetch w komponencie serwerowym, Next.js mógł automatycznie cache’ować dane i serwować je jako statyczne. Od teraz, żeby coś było cache’owane, musisz to wyraźnie zadeklarować.

Co to oznacza w praktyce?

Przykład – załóżmy, że masz funkcję, która pobiera dane z API:

async function getUser(id: string) {
  const res = await fetch(`https://api.example.com/users/${id}`);
  return res.json();
}

W Next.js 14 ta funkcja mogłaby być automatycznie cache’owana. W Next.js 15 – nie. Dane będą pobierane na nowo przy każdym żądaniu, chyba że dasz sygnał, że chcesz je cache’ować.

Aby ponownie uruchomić mechanizm cache’owania – używasz nowej dyrektywy 'use cache' (o niej więcej za chwilę).

Szukasz zaufanego wykonawcy projektów IT?
Szukasz zaufanego wykonawcy projektów IT?
Szukasz zaufanego wykonawcy projektów IT?
Skontaktuj się z nami!

Nowość: dynamicIO

Next.js 15 wprowadza też eksperymentalny tryb o nazwie dynamicIO, który zmienia sposób, w jaki framework traktuje dynamiczne funkcje. Domyślnie Next.js uważa, że wszystko jest dynamiczne (czyli wymaga SSR), ale z dynamicIO można oznaczyć konkretne części aplikacji jako bezpieczne do cache’owania lub wręcz statyczne – co daje duże zyski wydajnościowe.

Aby go aktywować, wystarczy dodać do next.config.js:

experimental: {
  serverActions: true,
  dynamicIO: true
}

To otwiera drzwi do głębszego zarządzania tym, co jest dynamiczne, a co nie – i pozwala osiągnąć równowagę między świeżością danych a wydajnością.

Dyrektywa 'use cache' – pełna kontrola nad cache

Nowa dyrektywa 'use cache' to jedno z najważniejszych narzędzi w Next.js 15. Dzięki niej możemy jednoznacznie określić, które funkcje powinny być cache’owane – i jak długo.

Jak działa 'use cache'?

To po prostu specjalna linia dodana na początku pliku (lub funkcji), która mówi: „ta funkcja ma być cache’owana”.

'use cache';

export async function getUser(id: string) {
  const res = await fetch(`https://api.example.com/users/${id}`);
  return res.json();
}

Od tego momentu getUser staje się funkcją, której wynik jest przechowywany w cache i nie jest odpytywany ponownie przy każdym żądaniu – dopóki nie zostanie unieważniony lub nie minie czas życia cache’u (jeśli taki zdefiniowaliśmy).

Gdzie można używać 'use cache'?

Use cache można używać w:

– w funkcjach asynchronicznych
– w komponentach serwerowych
– w helperach typu „data loader”
– na poziomie całych layoutów lub stron

Przykład z komponentem:

'use cache';

export default async function Profile({ userId }: { userId: string }) {
  const user = await getUser(userId); // również z 'use cache'
  return <div>{user.name}</div>;
}

W tym przypadku komponent oraz funkcja getUser są cache’owane, co oznacza, że dane użytkownika będą pobierane tylko raz na czas życia cache’a.

Konfiguracja projektu Next.js 15 z dynamicIO

Next.js 15 wprowadza nowy mechanizm o nazwie dynamicIO, który pozwala na bardziej precyzyjne zarządzanie tym, co może być cache’owane, a co powinno pozostać dynamiczne. To narzędzie nie tylko poprawia wydajność, ale także upraszcza złożone scenariusze, gdzie część danych może być statyczna, a część dynamiczna – i to w obrębie jednej strony lub komponentu.

Jak włączyć dynamicIO?

Aby aktywować dynamicIO, należy dodać odpowiednią flagę w pliku next.config.js. W praktyce wygląda to tak:

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    serverActions: true,
    dynamicIO: true,
  },
};

module.exports = nextConfig;

Uwaga praktyczna: dynamicIO działa wyłącznie w aplikacjach korzystających z App Routera (/app), nie ze starszym Pages Routerem (/pages). Warto też upewnić się, że masz aktualną wersję Next.js 15 i Node 18+.

Co zmienia dynamicIO?

W skrócie:

  • pozwala Next.js analizować przepływ danych w czasie kompilacji.
  • umożliwia korzystanie z 'use cache' w sposób optymalny – framework wie, które funkcje mogą być bezpiecznie cache’owane.
  • pozwala unikać przypadkowego SSR – co często było problemem w przeszłości (np. przez przypadkowe użycie headers() lub cookies()).

Praktyczny przykład

Załóżmy, że masz stronę /blog/[slug], która wyświetla wpis na podstawie slug-a. Chcesz, żeby treść posta była cache’owana (bo rzadko się zmienia), ale komentarze były zawsze aktualne (czyli dynamiczne).

Struktura katalogu:

app/
 └── blog/
      └── [slug]/
            └── page.tsx

getPost.ts (cache’owana funkcja):

'use cache';

export async function getPost(slug: string) {
  const res = await fetch(`https://cms.example.com/api/posts/${slug}`);
  if (!res.ok) throw new Error('Post not found');
  return res.json();
}

getComments.ts (zawsze dynamiczna):

export async function getComments(slug: string) {
  const res = await fetch(`https://cms.example.com/api/comments/${slug}`, {
    cache: 'no-store', // wymuszenie braku cache
  });
  return res.json();
}

page.tsx:

import { getPost } from '@/lib/getPost';
import { getComments } from '@/lib/getComments';

export default async function BlogPage({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);      // użyje cache
  const comments = await getComments(params.slug);  // dynamiczne

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>

      <section>
        <h2>Komentarze</h2>
        <ul>
          {comments.map((c) => (
            <li key={c.id}>{c.text}</li>
          ))}
        </ul>
      </section>
    </article>
  );
}

Efekt: Strona zostanie wygenerowana szybciej, ponieważ główny content jest cache’owany, ale użytkownik zobaczy zawsze aktualne komentarze.

Wskazówka: Gdy masz dużo danych do cache’owania, zadbaj o separację warstw:

– komponent (page.tsx) – scala te dane
Taki podział pozwala testować i debugować zachowanie cache bez zbędnego chaosu w logice widoku.

– warstwa danych (lib/get*.ts) – oznaczasz 'use cache' lub nie

Cache w praktyce – integracja Next.js 15 z lokalnym API

Nie musisz korzystać z zewnętrznych usług ani skomplikowanego backendu, aby przetestować działanie cache w Next.js 15. Wystarczy lokalny endpoint API, który zwraca dane – np. listę produktów. To wystarczające, by zademonstrować, jak działają 'use cache', rewalidacja oraz sposób, w jaki Next.js przechowuje dane na poziomie serwera.

Załóżmy, że budujesz stronę z produktami. Dane pochodzą z lokalnego endpointu /api/products, który zwraca listę produktów z pliku JSON (symulujemy tu backend).

1. API z danymi produktów (/api/products/route.ts)

Zacznijmy od lokalnego endpointu, który udostępnia listę produktów. Będzie to nasze źródło danych – symulacja backendu. Dane zwracamy w postaci JSON.

// app/api/products/route.ts
import { NextResponse } from 'next/server';

const products = [
  { id: 1, name: 'Laptop', price: 4500 },
  { id: 2, name: 'Klawiatura', price: 350 },
  { id: 3, name: 'Mysz', price: 150 },
];

export async function GET() {
  return NextResponse.json(products);
}

2. Funkcja pobierająca dane z API (lib/getProducts.ts)

Tworzymy osobną funkcję do pobierania danych. To w niej oznaczymy cache za pomocą 'use cache' oraz określimy czas rewalidacji.

// lib/getProducts.ts
'use cache';

export async function getProducts() {
  const res = await fetch('http://localhost:3000/api/products', {
    next: { revalidate: 60 }, // cache na 60 sekund
  });
  return res.json();
}

3. Strona z listą produktów (app/products/page.tsx)

Na koniec – strona, która wykorzystuje naszą funkcję i wyświetla dane w UI. Dane będą pobierane tylko raz na 60 sekund (dzięki cache), a potem serwowane z pamięci.

// app/products/page.tsx
import { getProducts } from '@/lib/getProducts';

export default async function ProductsPage() {
  const products = await getProducts();

  return (
    <section>
      <h1>Nasze produkty</h1>
      <ul>
        {products.map((p) => (
          <li key={p.id}>
            {p.name} – {p.price} zł
          </li>
        ))}
      </ul>
    </section>
  );
}

Użytkownicy odwiedzający stronę w krótkich odstępach czasu zobaczą dane z cache, co przyspieszy ładowanie i zmniejszy obciążenie serwera.

Zarządzanie czasem życia cache i rewalidacją danych

Samo cache’owanie danych to dopiero początek. W prawdziwych aplikacjach potrzebujemy sposobów, by określić, kiedy dane powinny się odświeżyć, i móc to kontrolować w sposób precyzyjny i przewidywalny.

Next.js 15 wprowadza kilka mechanizmów, które pozwalają zarządzać cyklem życia cache:

  • revalidate – czas przechowywania danych (np. 60 sekund),
  • cacheTag – tagowanie danych w cache’u,
  • revalidateTag – dynamiczne unieważnianie danych po stronie serwera.

Poniżej przejdziemy przez każdy z tych mechanizmów z praktycznymi przykładami.

1. revalidate – czas życia danych w cache

Pole revalidate określa, jak długo (w sekundach) wynik danego fetcha ma być przechowywany w pamięci cache zanim zostanie ponownie pobrany.

Przykład:

// lib/getNews.ts
'use cache';

export async function getNews() {
  const res = await fetch('https://api.example.com/news', {
    next: { revalidate: 120 }, // cache przez 2 minuty
  });
  return res.json();
}

Oznacza to: dane są cache’owane przez 2 minuty. Po tym czasie Next.js może pobrać świeżą wersję, ale stara wersja wciąż może być serwowana do czasu aktualizacji – tzw. stale-while-revalidate.

Wskazówka: używaj revalidate do danych, które rzadko się zmieniają, np. posty blogowe, produkty, kategorie.

2. cacheTag – tagowanie danych w cache

Gdy chcesz grupować i kontrolować cache dla powiązanych danych (np. wszystkie posty bloga), możesz przypisać im tagi.

Przykład:

// lib/getPosts.ts
'use cache';

export async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: {
      tags: ['posts'], // nadajemy tag
    },
  });
  return res.json();
}

To sprawia, że Next.js zapisuje wynik tego zapytania z etykietą "posts".

3. revalidateTag – dynamiczne unieważnienie cache

A teraz najlepsze: możesz zdalnie unieważnić cache z określonym tagiem. To szczególnie przydatne, jeśli Twoja aplikacja ma panel admina, z którego edytujesz dane.

Przykład:

// app/api/revalidate/posts/route.ts
import { revalidateTag } from 'next/cache';
import { NextResponse } from 'next/server';

export async function POST() {
  revalidateTag('posts'); // unieważnij wszystkie fetch’e z tagiem 'posts'
  return NextResponse.json({ success: true });
}

Gdy teraz użytkownik opublikuje nowego posta, możesz wywołać to API z poziomu panelu administracyjnego albo webhooka – a Next.js usunie odpowiedni wpis z cache i przy kolejnym żądaniu pobierze świeże dane.

Praktyczne przykłady – cache w layoutach, komponentach i funkcjach

Next.js 15 pozwala stosować cache nie tylko w funkcjach typu fetch, ale także w komponentach serwerowych, layoutach stron, a nawet w asynchronicznych „helperach”. W tej sekcji zobaczysz, jak skutecznie wdrażać 'use cache' na różnych poziomach architektury aplikacji.

1. Cache na poziomie layoutu – idealne dla powtarzalnych treści

Layouty w App Routerze to naturalne miejsce na wykorzystanie cache, ponieważ często zawierają stałe struktury takie jak nagłówki, stopki, sidebary czy menu.

// app/(main)/layout.tsx
'use cache';

import { getMenuItems } from '@/lib/getMenuItems';

export default async function MainLayout({ children }: { children: React.ReactNode }) {
  const menu = await getMenuItems(); // cache'owana funkcja

  return (
    <div>
      <nav>
        <ul>
          {menu.map((item) => (
            <li key={item.href}>
              <a href={item.href}>{item.label}</a>
            </li>
          ))}
        </ul>
      </nav>
      <main>{children}</main>
    </div>
  );
}

Menu jest pobierane tylko raz (lub zgodnie z revalidate), dzięki czemu layout nie wykonuje zbędnych zapytań przy każdej stronie.

2. Komponenty serwerowe z cache – np. widżety lub hero

Często masz komponenty, które pojawiają się na wielu podstronach i nie zmieniają się zbyt często — np. baner z promocją, widżet pogody, blok „polecane artykuły”.

Przykład:

// components/HeroBanner.tsx
'use cache';

import { getPromoBanner } from '@/lib/getPromoBanner';

export default async function HeroBanner() {
  const banner = await getPromoBanner();

  return (
    <section className="hero">
      <h1>{banner.title}</h1>
      <p>{banner.subtitle}</p>
    </section>
  );
}

W ten sposób komponent sam zarządza swoim cache – i może być używany w wielu miejscach, bez duplikacji logiki.

3. Asynchroniczne funkcje z cache – logika oddzielona od widoku

Jeśli chcesz odseparować warstwę danych od warstwy prezentacji, warto cache’ować funkcje pomocnicze (data loaders), które zwracają tylko dane – a komponenty tylko je renderują.

Przykład:

// lib/getRecommendedPosts.ts
'use cache';

export async function getRecommendedPosts(category: string) {
  const res = await fetch(`https://api.example.com/posts?category=${category}`, {
    next: { revalidate: 300 },
    // optional: tags: ['recommended'],
  });
  return res.json();
}

I użycie:

// components/RecommendedSection.tsx
import { getRecommendedPosts } from '@/lib/getRecommendedPosts';

export default async function RecommendedSection({ category }: { category: string }) {
  const posts = await getRecommendedPosts(category);

  return (
    <aside>
      <h2>Polecane artykuły</h2>
      <ul>
        {posts.map((p) => (
          <li key={p.id}>{p.title}</li>
        ))}
      </ul>
    </aside>
  );
}

Dzięki temu komponent nie musi wiedzieć nic o cache – to zadanie getRecommendedPosts.

Wyzwania i najlepsze praktyki – czyli jak nie wpaść w pułapki cache

W poprzednich częściach pokazaliśmy, jak wdrażać cache w Next.js 15 krok po kroku. Teraz pora na spojrzenie praktyczne: co może pójść nie tak, gdzie czają się typowe błędy i jak podejść do cache’owania w sposób świadomy, przewidywalny i skalowalny.

Next.js 15 daje ogromną moc – ale jak każda zaawansowana funkcja, wymaga zrozumienia i dyscypliny. Poniżej znajdziesz krótką listę najczęstszych problemów oraz sprawdzone zasady, które pomogą Ci budować szybkie, stabilne i dobrze zarządzane aplikacje.

Najczęstsze problemy i błędy

  • fetch się nie cache’uje mimo 'use cache' – bo użyto funkcji dynamicznych (headers(), cookies()).
  • nieświadome nadpisywanie cache – przez brak revalidate lub błędne tagi.
  • dane są zbyt długo cache’owane – brak automatycznego odświeżenia.
  • brak unieważniania cache po edycji danych – np. przez panel admina.
  • próba cache’owania komponentu klientowego ('use client') – co nie działa.
  • nadmiar cache’owanych funkcji – co prowadzi do niepotrzebnego skomplikowania logiki.

Najlepsze praktyki

  • zacznij od cache’owania danych, nie komponentów – najpierw fetch, potem layout.
  • stosuj revalidate dla danych zmiennych, np. co 60–300 sekund.
  • taguj dane (cacheTag), jeśli chcesz mieć możliwość unieważniania konkretnej grupy.
  • cache’uj tylko to, co naprawdę warto – nie wszystko musi być przechowywane.
  • oddziel logikę od renderowania – komponent powinien tylko wyświetlać dane, nie decydować o ich świeżości.
  • twórz osobne funkcje dla danych dynamicznych i statycznych – nie mieszaj podejść.
  • dokumentuj decyzje cachingowe – łatwiej to utrzymać w zespole.
  • testuj zachowanie cache przy development buildzie (next dev) i production buildzie (next build && start) – mogą się różnić!

Cache w Next.js 15 to nie tylko narzędzie optymalizacji, ale nowy sposób myślenia o strukturze aplikacji. Rezygnacja z automatycznego cache’owania i wprowadzenie takich mechanizmów jak 'use cache', revalidate, cacheTag czy dynamicIO daje programiście realną kontrolę nad wydajnością i aktualnością danych.

W artykule pokazaliśmy, jak:

  • działa nowy model cache’owania w Next.js 15,
  • wdrażać cache w funkcjach, komponentach i layoutach,
  • zarządzać cyklem życia danych i ich rewalidacją,
  • unikać najczęstszych błędów i stosować dobre praktyki.

Nowy system wymaga większej świadomości, ale też daje większą przewidywalność i elastyczność. Dzięki temu możesz zbudować aplikację, która nie tylko działa szybko, ale też dostarcza użytkownikom aktualnych i wiarygodnych danych — dokładnie wtedy, kiedy trzeba.

Powiązane artykuły
Zobacz wszystkie
Odkryj więcej tematów