Hoe WordPress caching werkt

WordPress caching is niet één ding. Het zijn minstens vier losse lagen, elk met een eigen probleem, eigen failure modes en een eigen plek om te configureren. In dit artikel leg ik uit wat elke laag doet, welke laag een caching plugin eigenlijk bedoelt als die het over caching heeft en welke problemen caching helemaal niet oplost.

Zegt iemand "ik heb een caching plugin geïnstalleerd", dan heeft hij het bijna altijd over page caching. Zegt iemand "mijn host heeft Redis voor me aangezet", dan gaat het over object caching. Zegt iemand "leeg even je browsercache, dan is je site weer snel", dan gaat het over een derde ding dat helemaal niets doet aan serverperformance. Dit zijn geen alternatieven van elkaar. Het zijn lagen, elk met een eigen taak, en een goed afgestelde WordPress-site gebruikt er meerdere tegelijk. Dit artikel gaat over dat model: wat elke laag precies cachet, wanneer het helpt, wanneer het in de weg zit en welke problemen caching überhaupt niet oplost, hoe je de lagen ook stapelt.

De drie hoofdlagen op een WordPress-site

Een productie-WordPress-response gaat meestal door drie cachelagen voordat een bezoeker hem ziet, plus een vierde die er nog voor zit (het CDN). Elke laag cachet iets anders, op een ander moment, met een andere levensduur.

Laag Wat wordt gecachet Wie serveert het Overleeft een PHP-crash Per gebruiker
Page cache (full-page) Complete HTML-response Webserver of PHP-plugin Ja Nee (bypass voor ingelogd)
Object cache (persistent) Databasequery-resultaten, berekende waardes Redis of Memcached Ja Nee
Object cache (niet-persistent) Databasequery-resultaten binnen één request PHP zelf, in-memory Nee (per request) Nee
Browser cache Statische assets (CSS, JS, afbeeldingen, fonts) De browser van de bezoeker Ja Ja (per apparaat)
CDN edge cache Statische assets en soms HTML Edge server dichtbij de bezoeker Ja Nee

De regel die dit geheel logisch maakt: elke laag cachet iets dat dichter bij de output zit dan de laag eronder, en elke laag slaat meer werk over dan de laag eronder. Een page cache slaat het meeste werk over (er draait helemaal geen PHP). Een browser cache slaat het minste over (de browser voorkomt alleen dat hij een bestand dat hij al heeft opnieuw downloadt). De rest zit daar tussenin.

De WordPress advanced administration handbook over caching framet het op dezelfde manier: "moving data from a place of expensive and slow retrieval to a place of cheap and fast retrieval." De lagen bestaan omdat er meerdere "trage plekken" in een WordPress-paginabuild zijn, en je kunt ze niet allemaal vanuit één punt bereiken.

Wat page caching eigenlijk doet

Page caching bewaart de complete HTML-response voor een URL en geeft die HTML terug aan de volgende bezoekers zonder dat PHP of de database nog wordt aangeroepen. Op een ongecachete aanvraag boot WordPress, laden alle actieve plugins, draaien er tientallen databasequeries en stelt het thema de HTML samen. Bij een cache hit gebeurt niets van dat alles. De webserver leest een kant-en-klare HTML-file en stuurt die op.

Dit is de laag die de spectaculaire "mijn site is tien keer sneller" cijfers oplevert. De WordPress-handbook beschrijft het als "reducing the processing load on the server" en merkt op dat page caching "can improve performance several hundredfold for relatively static content" (bron). De reden dat die cijfers ook echt kloppen, is dat PHP-executie en databasequeries samen het grootste deel van de TTFB van een WordPress-pagina uitmaken, en page caching slaat beide over.

Page caching komt in twee smaken:

  • Plugin-gedreven page caching. Een plugin als WP Rocket, WP Super Cache, W3 Total Cache of LiteSpeed Cache maakt een wp-content/advanced-cache.php drop-in aan, die WordPress vroeg in het request laadt als define('WP_CACHE', true) in wp-config.php staat. De plugin schrijft statische HTML-bestanden naar disk (of geheugen) en serveert die bij volgende hits. PHP start nog wel op, maar stopt er vroeg mee.
  • Server-level page caching. nginx fastcgi_cache, Varnish of LiteSpeed LSCache leveren gecachete HTML direct vanuit de webserver, voordat PHP überhaupt wordt aangeroepen. Een typische nginx fastcgi_cache configuratie bewaart pagina's op disk met een keys_zone-directive en levert ze zolang er geen bypass-conditie actief is. Dit is sneller dan plugin caching omdat PHP nooit wordt gestart, en het is de aanpak die managed WordPress-hosts meestal gebruiken.

Wanneer page caching wordt omzeild (als caching niet werkt zoals verwacht, zie WordPress-cache werkt niet voor het troubleshooting-pad)

Page caching werkt alleen als dezelfde HTML klopt voor iedere bezoeker. Zodra een pagina per bezoeker anders moet zijn, moet de cache opzij gaan. Elke serieuze page cache omzeilt zichzelf voor:

  • Ingelogde gebruikers. De admin bar, persoonlijke begroetingen en previews van concepten hangen allemaal af van wie er kijkt. Een gecachete kopie aan een ingelogde redacteur serveren zou de uitgelogde versie laten zien, en dat is precies waarom de WordPress-admin traag aanvoelt zelfs op sites waar de publieke voorkant snel is.
  • POST-requests. Formulier-submits, reacties en WooCommerce checkout-stappen moeten altijd naar live PHP.
  • URL's met query strings die het antwoord beïnvloeden. Zoekresultaten, gefilterde productarchieven.
  • WooCommerce winkelwagen- en checkoutpagina's. De eigen documentatie van WooCommerce is expliciet: cart, my account en checkout "need to stay dynamic since they display information specific to the current customer and their cart." De cookies die een bypass moeten triggeren zijn onder meer woocommerce_cart_hash, woocommerce_items_in_cart en wp_woocommerce_session_. Dit is ook de belangrijkste reden dat page caching een trage WooCommerce checkout niet oplost: de checkout wordt per definitie niet gecachet.
  • Feeds, sitemaps, wp-admin, xmlrpc. Die hebben hun eigen regels.

Een goed geschreven cache plugin regelt dit allemaal automatisch. Een slecht afgestelde plugin cachet de winkelwagenpagina en serveert de cart van vreemde A aan vreemde B. Ik heb dat ook letterlijk zien gebeuren op een WooCommerce-site waar iemand twee page cache plugins had geïnstalleerd en de tweede niets wist van de bypass-regels van de eerste.

Object caching: transients en de persistent object cache

Page caching helpt als je de hele HTML kunt hergebruiken. Object caching helpt als je alleen stukjes van de pagina kunt hergebruiken, of als twee verschillende pagina's hetzelfde dure queryresultaat delen.

WordPress heeft al sinds versie 2.0 een object cache. Standaard is die niet-persistent: hij leeft in het PHP-geheugen voor de duur van één request en wordt weggegooid zodra het request stopt. Ook dat helpt al, want een typische paginabuild roept get_option() en get_post_meta() vele keren aan, en de tweede keer binnen hetzelfde request komt uit geheugen. De WP_Object_Cache class reference zegt het duidelijk: "Cached data will not be stored persistently across page loads unless you install a persistent caching plugin."

Een persistent object cache is wat de meeste mensen bedoelen als ze zeggen "installeer Redis voor WordPress" of "zet Memcached aan". Een drop-in bestand op wp-content/object-cache.php vervangt de standaard object cache door een backend (Redis of Memcached) die over requests heen blijft bestaan. Query-resultaten die in request A worden gecachet, zijn in request B weer bruikbaar. De handbook noemt object caching "moving data from a place of expensive and slow retrieval to a place of cheap and fast retrieval", en dat is precies wat een persistent backend doet: data die anders uit MySQL moet komen, verhuist naar Redis waar hij in minder dan een milliseconde op te halen is.

De transients API en waarom "transients zijn niet de database" belangrijk is

De Transients API is de ingebouwde manier waarop WordPress een waarde met een vervaltijd bewaart. Op een site zonder persistent object cache staan transients in de wp_options tabel met een _transient_ prefix en een bijbehorende _transient_timeout_ key. Op een site mét een persistent object cache worden transients in plaats daarvan in de object-cache-backend bewaard en wordt wp_options helemaal overgeslagen.

Daarom waarschuwt de Transients API documentatie ook: "Transients should also never be assumed to be in the database, since they may not be stored there at all." En er staat nog een waarschuwing die ertoe doet: "Transient expiration times are a maximum time. There is no minimum age." Loopt de object cache vol, dan kan hij een transient evicten nog voordat de TTL om is, dus elke code die transients gebruikt moet zó geschreven zijn dat de waarde opnieuw kan worden opgebouwd als hij er niet is. Een plugin die aanneemt dat zijn transient er altijd is, geeft stale of lege resultaten zodra Redis zijn maxmemory plafond raakt.

Wat de recente WordPress-releases nu precies veranderd hebben

De object cache API is per versie duidelijk geëvolueerd, en het helpt om dat aan specifieke versienummers vast te pinnen. De versie-tabel op de WP_Object_Cache reference pagina vermeldt elke toevoeging:

  • WordPress 6.0 (mei 2022) voegde batch cache-functies toe: wp_cache_add_multiple(), wp_cache_set_multiple(), wp_cache_delete_multiple() en wp_cache_flush_runtime(). Plugins en core kunnen nu veel keys tegelijk ophalen of zetten in één round trip naar de backend, in plaats van één per key (officiële dev notes).
  • WordPress 6.1 (november 2022) introduceerde wp_cache_supports() en wp_cache_flush_group() (function reference), zodat cache-implementaties een formele manier hebben om hun capabilities te declareren. 6.1 voegde ook twee Site Health checks toe: Persistent Object Cache (suggereert Redis of Memcached als de schaal van de site daarom vraagt) en Full Page Cache (detecteert full-page caching en toetst de response time tegen een drempel van 600 ms). De Make WordPress-post die deze checks aankondigt is de primaire bron. Belangrijk: 6.1 veranderde niet hoe de object cache intern werkt. Het gaf derde-partij object cache plugins een gestandaardiseerde manier om hun capabilities bekend te maken en beheerders zicht op de vraag of er überhaupt een persistent cache draait.
  • WordPress 6.3 (augustus 2023) was de release met de grotere interne wijzigingen. Het introduceerde aparte query cache groups (post-queries, term-queries, comment-queries) en wp_cache_set_last_changed(), zodat WP_Query-resultaten, term-queries en comment-queries in benoemde groepen cached konden worden en gericht gevlushed konden worden zodra de onderliggende data wijzigt. De 6.3 dev notes leggen dit in detail uit.
  • WordPress 6.4 (november 2023) herordende de cache-lookup in get_option() zodat eerst de object cache wordt geraadpleegd en daarna pas de database, forceerde split queries in WP_Query als er een persistent object cache actief is en voegde een cache_results parameter toe aan WP_Term_Query (dev notes). Het netto-effect is dat meer intern werk van WordPress de object cache daadwerkelijk gebruikt, áls die tenminste geïnstalleerd is, vanaf versie 6.4.

De samenvatting voor een site-eigenaar: draai je WordPress 6.4 of nieuwer én heb je een persistent object cache, dan doet die cache nu meer werk voor je dan op 6.0 het geval was. Draai je 6.4 zonder persistent object cache, dan profiteer je van geen van deze verbeteringen, want er is geen backend waar ze hun werk in kwijt kunnen.

Browser caching en de Cache-Control header

Browser caching is de laag waar de meeste mensen het meeste over in de war zijn. Het wordt niet door een plugin geregeld. Het is niet iets wat WordPress doet. Het is een afspraak tussen de webserver en de browser van de bezoeker, via HTTP response headers, die zegt: "je hebt dit bestand al, gebruik je eigen kopie in plaats van hem opnieuw te downloaden."

De headers die ertoe doen staan allemaal in het HTTP cache artikel van web.dev:

  • Cache-Control: max-age=31536000 vertelt de browser dat hij het bestand tot een jaar lang mag hergebruiken (31.536.000 seconden is de maximale praktische waarde). Dit past bij versioned of gehashte statische assets: een bestand dat main.a3f2b1.css heet kan niet betekenisvol veranderen, want elke wijziging levert een nieuwe bestandsnaam op.
  • Cache-Control: no-cache betekent niet "nooit cachen". Het betekent "check eerst even bij de server voordat je de gecachete kopie gebruikt" (revalidatie). Dit past bij HTML-responses, zodat een browser met de HTML van gisteren niet die oude versie laat zien zodra er vandaag nieuwe HTML is.
  • Cache-Control: no-store betekent dat browser en alle intermediate caches (inclusief CDN's) überhaupt geen kopie mogen bewaren. Past bij bankafschriften en sessie-specifieke persoonlijke data.
  • immutable signaleert dat het bestand nooit verandert; de browser kan revalidatie helemaal overslaan. web.dev waarschuwt wel: "immutable will be ignored in some browsers", dus het is een optimalisatie, geen garantie.

Het belangrijkste aan browser caching is juist wat het niet doet voor een WordPress-site. Het werkt alleen bij herhaalbezoek van dezelfde bezoeker. Een eerste bezoeker heeft een lege browsercache en downloadt alles. Daarbij geldt browser caching alleen voor bestanden die de server al heeft verstuurd, wat betekent dat het niks doet voor TTFB, PHP-executietijd, databasequerytijd of server-side page cache hits. Daarom is het vaak gehoorde advies "leeg even je browsercache om die trage site op te lossen" ook zo misleidend: je browsercache legen forceert juist een redownload, en dat is het tegenovergestelde van sneller. Browser caching maakt het tweede bezoek goedkoper. Het heeft geen effect op het eerste bezoek en geen effect op de server.

CDN edge caching: waarin het verschilt van server-side caching

Een CDN (Cloudflare, Fastly, Bunny, KeyCDN en dergelijke) zet in tientallen geografische locaties een cache server neer, zodat een bezoeker uit Sydney niet eerst helemaal naar Amsterdam hoeft. Edge caching is iets anders dan de page cache op de origin:

  • Wat een CDN altijd cachet. Statische assets (CSS, JS, afbeeldingen, fonts) die met een lange Cache-Control: max-age worden geserveerd. Dit is de makkelijke winst en de reden dat elke serieuze WordPress-site een CDN voor statische assets zou moeten hebben.
  • Wat een CDN soms cachet. Hele HTML-pagina's, maar alleen als het expliciet wordt ingesteld (Cloudflare's "cache everything" page rule, Fastly met custom VCL, Bunny Perma-Cache). Dit is in feite het verplaatsen van de full-page cache van de origin naar de edge. De bypass-regels zijn dan hetzelfde als op origin-niveau: cart, checkout, ingelogde gebruikers.
  • Wat een CDN bijna nooit cachet. Dynamische HTML voor ingelogde gebruikers, REST API responses, admin requests. Die omzeilen de edge cache en leggen alsnog de volle afstand naar de origin af.

De klassieke misvatting: "ik heb een CDN toegevoegd maar mijn WooCommerce checkout is nog steeds traag." Een CDN maakt statische assets en cachebare HTML sneller voor bezoekers ver van je origin. Het doet niets aan een trage origin op dynamische pagina's, want die requests moet het CDN toch nog doorsturen naar de origin. Is je site traag omdat PHP en de database traag zijn, dan brengt een CDN het probleem alleen maar dichter bij de bezoeker; verwijderen doet het niks. Zie het artikel "WooCommerce is traag" voor waarom het checkoutpad sowieso slecht met caching samengaat.

Een caching plugin kiezen (en wat die plugin dan echt doet)

Als iemand vraagt "welke caching plugin moet ik nemen?", dan is de eerlijke tegenvraag: "welke laag heb je nodig?" De meeste grote plugins concurreren op de page cache laag (en in mindere mate op browser-header management en CDN-integratie). Ze vervangen een persistent object cache niet; ze werken ernaast.

Dat meerdere page cache plugins met elkaar botsen, staat goed gedocumenteerd op de WordPress performance tracker en in een vaak geciteerde LiteSpeed blogpost: het wp-content/advanced-cache.php bestand is een drop-in, en maar één plugin kan er eigenaar van zijn. Proberen twee page cache plugins allebei dat bestand te schrijven, dan overschrijft de tweede de eerste, denken ze allebei de baas te zijn en breekt de cache-invalidation op een manier die lastig te debuggen is. De algemene regel: precies één page cache. Je kunt prima één page cache plugin combineren met een aparte object cache plugin (Redis Object Cache of een equivalent), want die richten zich op verschillende drop-ins (advanced-cache.php versus object-cache.php).

Wat caching NIET is

De meeste verwarring rond WordPress caching komt voort uit het idee dat het één ding is dat óf werkt óf niet werkt. Dat is het niet. Dit zijn de specifieke dingen die caching niet is.

  • Geen enkel ding. "Zet caching aan" is vier verschillende antwoorden, afhankelijk van de laag die je bedoelt. Een caching plugin regelt meestal page caching en soms ook browser-cache headers. Hij geeft je op zichzelf geen object caching, tenzij je Redis of Memcached apart installeert. En hij geeft je geen CDN caching tenzij je een CDN afneemt.
  • Geen oplossing voor een trage WooCommerce checkout. De checkout wordt per ontwerp en per WooCommerce-documentatie nooit vanuit page cache geserveerd. Object caching versnelt wel de databasequeries achter de productpagina's en de productcatalogus, maar cachet de checkout-response zelf niet. Is je checkout traag, dan is caching de verkeerde laag om naar te kijken. Focus op PHP-executie, database-indexen en de payment-gateway API calls in het checkoutpad.
  • Niet op te lossen door je browsercache te legen. De browsercache zit het verst van de server af en heeft geen invloed op hoe lang de server doet over een antwoord. Hem legen forceert juist een redownload, wat het tegenovergestelde van sneller is. Het enige wat je browsercache legen oplost is een stale asset die deze specifieke browser al had opgeslagen.
  • Niet "meer is altijd beter". Twee page cache plugins is niet twee keer zoveel caching. Het is een race condition rond advanced-cache.php. Gebruik per laag precies één plugin. Object caching (Redis) en page caching (WP Rocket, bijvoorbeeld) kunnen prima naast elkaar, want ze richten zich op verschillende drop-ins. Twee plugins op dezelfde laag kan niet.
  • Geen vervanging voor het fixen van de onderliggende code. Een plugin die op elke paginabuild een synchrone remote API call doet, blijft op elke cache miss alsnog die remote API raken. Caching helpt voor de gecachete hits; voor alles wat de cache omzeilt helpt het niet, en op een WooCommerce-site omzeilt een flink deel van het verkeer de cache. Caching koopt je ruimte. Het vervangt geen efficiënte code.
  • Niet gratis. Een persistent object cache kost geheugen (Redis houdt alles standaard in RAM). Een page cache kost diskruimte. Een CDN kost geld. En elke cache heeft een invalidation probleem: zodra de onderliggende data verandert, moet de cache dat weten. Een site die te agressief cachet serveert verouderde prijzen, verkeerde voorraad of achterhaalde content. Het bekende citaat van Phil Karlton over de twee moeilijke problemen in computer science noemt cache invalidation niet voor niets.

Waar je verder kunt kijken

Kwam je hier omdat je server ook op simpele pagina's traag is? Dan legt het artikel over hoge TTFB uit wat die meting eigenlijk bevat en welke cachelaag welk deel ervan aanpakt. Voelt de hele site traag aan en wil je eerst uitzoeken in welke laag de bottleneck zit, dan doorloopt het artikel "Waarom voelt een WordPress-site traag" hoe je een request opknipt in zijn netwerk-, server- en browserfase. Is het specifiek de WooCommerce checkout of cart die traag is, dan legt het artikel "WooCommerce is traag" uit waarom de dynamische pagina's niet te cachen zijn en waar je dan wél moet kijken.

Klaar met terugkerende traagheid?

Traagheid komt vaak terug na snelle fixes. Professioneel onderhoud houdt updates, caching en limieten consequent op orde.

Bekijk WordPress onderhoud

Doorzoek deze site

Begin met typen om te zoeken, of blader door de kennisbank en blog.