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
Umów się na bezpłatną konsultację

    Twoje dane przetwarzamy zgodnie z naszą polityką prywatności.

    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