Logboek / Ontwikkeling van webproducten

Hoe de cache werkt in Next.js v13+ met de App Router

Hoe de cache werkt in Next.js v13+ met de App Router

Een gedetailleerd overzicht van hoe caching werkt in Next.js 13+, welke soorten cache er bestaan en hoe dat prestaties en SEO beïnvloedt.

Wat is cache en hoe werkt het in Next.js

Een cache slaat de resultaten van berekeningen of requests op, zodat ze niet opnieuw hoeven te worden uitgevoerd, wat de applicatie sneller maakt. In Next.js kunnen HTML-pagina's, data, requests, het renderen van componenten en zelfs hele routes in de cache worden bewaard.

Met de komst van de App Router (vanaf Next.js 13) is de filosofie veranderd: caching staat nu standaard aan, en het is aan de developer om het uit te schakelen wanneer dynamisch gedrag nodig is.

Het beleid van Next.js is als volgt:

"We cachen alles, tenzij anders aangegeven"

Als je niet expliciet cache: 'no-store', next: { revalidate } of dynamic = 'force-dynamic' opgeeft, dan:

  • wordt fetch() gecachet
  • kunnen layout en pagina ook gecachet worden
  • worden routes toegevoegd aan de routecache
  • gaat het renderen van de pagina over naar de volledige routecache

Dat zorgt voor snelheid, maar kan leiden tot verouderde data en lastig te debuggen problemen.

Soorten cache in Next.js

Bron: Next.js-documentatie

Volledige routecache (routecache aan de serverzijde)

Wordt gebruikt met ISR (Incremental Static Regeneration).

  • De hele pagina wordt gecachet (HTML, data uit layout.tsx, page.tsx, fetch(), headers(), cookies()).
  • De cache staat op de server.
  • Werkt standaard wanneer er geen dynamisch gedrag is.
  • Wordt uitgeschakeld met revalidate of revalidateTag().

Het beste voor blogs, marketingpagina's en andere content die zelden verandert.

Voorbeeld:

export const revalidate = 60; // regenerate the page every 60 seconds

Datacache (cache voor het ophalen van data aan de serverzijde)

  • Antwoorden van fetch() in Servercomponenten worden gecachet.
  • Te sturen met next: { revalidate } of cache: 'no-store'.

Voorbeeld:

await fetch('https://api.com/posts', {
  next: { revalidate: 300 },
});

Request-memoization (cache per request, aan de serverzijde)

Dit is een interne optimalisatie van Next.js: dezelfde fetch() of functie met dezelfde argumenten wordt per render maar één keer aangeroepen.

  • Binnen één serverrender houdt Next.js identieke requests bij.
  • Zijn ze al aangeroepen, dan komt het resultaat uit de cache.
  • Werkt onafhankelijk van cache: 'force-cache' of no-store.

Goed voor:

  • DRY (don't repeat yourself) bij het ophalen van dezelfde data in verschillende componenten.
  • Betere prestaties zonder handmatige optimalisatie.
  • Wanneer dezelfde fetch zowel in layout.tsx als in page.tsx wordt gebruikt.

Niet voor:

  • Hergebruik tussen verschillende requests of gebruikers (het is geen globale cache).
  • Het omzeilen van de datacache: werkt op basis van request-memoization.
// lib/getUser.ts
export async function getUser() {
  return fetch('https://api.com/user').then(res => res.json());
}

// app/layout.tsx
export default async function Layout({ children }) {
  const user = await getUser(); // Call 1
  return (
    <div>
      <Sidebar user={user} />
      {children}
    </div>
  );
}

// app/page.tsx
import { getUser } from '../lib/getUser';

export default async function Page() {
  const user = await getUser(); // Call 2, but actually not repeated
  return <p>Hello, {user.name}</p>;
}

Fysiek wordt fetch maar één keer aangeroepen en het resultaat wordt op beide plekken gebruikt.

Routercache (cache aan de clientzijde)

Dit is de cache die je zelf beheert met bibliotheken als SWR, React Query en andere.

  • Werkt alleen bij navigatie aan de clientzijde (<Link>, router.push()).
  • Next.js beheert deze cache niet: jij bepaalt hoe je haar onthoudt.
  • Heel snel, omdat er niets opnieuw wordt gerenderd: React mount gewoon vanuit het geheugen.
  • Maar ze bestaat alleen totdat de gebruiker de pagina herlaadt (F5).

Het beste voor interactieve componenten, formulieren en dashboards.

Voorbeeld:

'use client';
import useSWR from 'swr';

const { data } = useSWR('/api/data', fetcher);

Visuele vergelijking van de soorten cache

Soort cacheWaar het werktBesturingVoorbeeld
Volledige routecacheServer (hele route)revalidate, tagexport const revalidate = 60
DatacacheServer (fetch)cache, revalidatefetch(..., { next: ... })
Statisch gegenereerde cacheServer (buildtijd)GeenJSX zonder data
Cache aan de clientzijdeBrowser / clientJijSWR / React Query

Voorbeeld uit het dagelijks leven

Stel dat je een webshop bouwt:

PaginaWat te onthoudenWelke soort cache te gebruiken
/productenProductlijst, elke 5 minuten bijgewerktVolledige routecache (ISR)
/api/productenProductdata van de APIDatacache (met revalidateTag)
SPA-navigatieSnelle navigatieRoutercache (cache aan de clientzijde)

Veelvoorkomende cacheproblemen in de App Router

Data is bijgewerkt, maar de pagina toont oude data

fetch() gebruikt standaard force-cache als je geen cache: 'no-store' of next: { revalidate } opgeeft. Het gevolg: Next.js onthoudt de data en werkt haar nooit bij.

Oplossing:

  • Voeg cache: 'no-store' toe om caching uit te schakelen
  • Of next: { revalidate: 60 } voor ISR
fetch('https://api.com/data', { next: { revalidate: 60 } });

De URL (searchParams) is veranderd, maar de data blijft hetzelfde

Next.js voert de fetch-aanvraag niet opnieuw uit als de URL gelijk is. Gebruik je parameters, zet ze dan rechtstreeks in de fetch-URL:

fetch(`https://api.com/posts?category=${searchParams.category}`, {
  next: { revalidate: 60 }
});

Navigatie met <Link>, maar de data is niet bijgewerkt

De routercache (cache aan de clientzijde) is gebruikt. Next.js stuurt geen nieuwe request wanneer je terugkeert naar de pagina.

Oplossing:

  • Forceer verversing van de data met router.refresh()
  • Gebruik cache: 'no-store' als de data altijd vers moet zijn

revalidatePath() of revalidateTag() werkt niet

  • De cache is niet aangemaakt (bijvoorbeeld door no-store)
  • Je gebruikt geen tags in de fetch
  • Je roept revalidatePath() aan buiten een server action

Oplossing:

  • Zorg dat de fetch next: { tags: [...] } gebruikt
  • Roep revalidateTag('tag') aan in een serverfunctie

De layout wordt gecachet en wordt niet bijgewerkt

layout.tsx wordt ook gecachet als RSC met revalidate, en als de data erin gecachet is, wordt die pas bijgewerkt bij de volgende ISR.

Oplossing:

  • Gebruik cache: 'no-store' voor fetches in layout.tsx, als die bijvoorbeeld een gebruikersprofiel bevat
  • Of verplaats het ophalen van de layout naar de pagina (of naar een clientcomponent met SWR)

Onverwacht gedrag in de ontwikkelmodus (dev)

In next dev staan veel caches uit of werken ze anders, en revalidate werkt niet betrouwbaar in de ontwikkelmodus.

Tip:

Test caching met next build && next start of op een Vercel-preview.

Een combinatie van cookies(), headers(), params en fetch() breekt de cache.

Wanneer je dynamische dingen gebruikt zoals:

cookies()

headers()

useSearchParams()

generateMetadata() met dynamische waarden

schakelt Next.js de paginacache automatisch uit en gaat over op dynamic = 'force-dynamic'. Checklist: zo vermijd je cacheproblemen

Stel het fetchgedrag altijd duidelijk in:

  • Vergeet niet de queryparameters rechtstreeks in de fetch-URL op te nemen
  // no cache at all
  fetch(..., { cache: 'no-store' })

  // ISR (revalidate every 60 seconds)
  fetch(..., { next: { revalidate: 60 } })

  // manual invalidation via tag
  fetch(..., { next: { tags: ['products'] } })
  • Vergeet niet de queryparameters rechtstreeks in de URL te zetten die je aan fetch doorgeeft
  • Test met next build && next start, anders kun je een vals gevoel van zekerheid hebben
  • Moet je data handmatig ongeldig maken, gebruik dan revalidatePath() of revalidateTag()

Draait de cache in je project met de App Router al in productie en geeft hij problemen? Dat soort klussen pak ik op als freelance Next.js-developer: cache-audits, fixes en migraties naar de App Router.