
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.

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ę).
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()
lubcookies()
).
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 fetch
a 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.


