Hello Cache! Het praktisch nut van de WordPress Transients API

Eerder sprak ik al over de 7 zonden uit mijn carrière als developer, vandaag grijp ik terug op weer een kritiek puntje dat zich hieronder schaart. Lange tijd keek ik met een lange neus naar elke technologie die eender welke manier van Cache behelsde. Ik wilde de data die ik op mijn website publiceerde, live tonen zonder enige compromis. Wanneer je slechts een paar bezoekers bediend, is er geen vuiltje aan de lucht en ligt de data of dienst er in het ergste geval (na verloop van tijd) een keer uit waardoor er geen informatie kan worden getoond. Echter zodra je website wat meer traffic genereert kan het zijn dat je het aantal API calls overschrijd, maar het echte verschil werd voor mij merkbaar tijdens mijn werkzaamheden bij INDICIA en Dutchwebdesign waarbij klanten werden bediend met grotere belangen, bredere doelgroepen en meerdere (gelijktijdige) bezoekers. Op dat moment bedacht ik me, wellicht is het helemaal niet logisch om voor elk individu alle data live op te halen.

Wellicht is het helemaal niet logisch om voor elk individu alle data live op te halen.

Toch heeft het tot een interessante talk van Andreas Creten op WordCamp Antwerp 2016 geduurd alvorens ik enigszins geënthousiasmeerd raakte tot het gebruik van Caching, of in ieder geval de denkwijze achter Caching. Mijn ervaringen met Cache tot dat moment waren beperkt tot zogeheten “alles of niets” configuraties waardoor elke webpagina, elke vorm van dynamiek verloor en enkel (verouderde) statische content toonde. Iets wat natuurlijk geen optie is voor een Social Media feed, “Now Playing” koppeling met Spotify of “Last Beer” verwijzing vanuit Untappd.

Uiteraard zijn er zat mensen die zich hebben gespecialiseerd in Cache en zijn hier veel bredere, interessantere en diepgaandere posts over te vinden. Ook zal er een andere groep zijn die roept dat al je Caching debakels opgelost zijn door het installeren (en in mijn beleving ondergeschoven, maar niet onbelangrijk, juist configureren) van Cache plugins als W3TC en WP Super Cache. Deze laatste optie kan voor een informatieve website die weinig actueel is met een simpele basisconfiguratie een prima snelheidswinst bewerkstelligen. Maar dit komt vooral doordat zaken als het menu, de footer en voor jou belangrijke pagina’s als “over ons” en “contact” over het algemeen weinig zullen veranderen. Uit mijn eigen persoonlijke ervaring is echter gebleken dat dynamische content als live feeds met data uit bijvoorbeeld Social Media platformen of externe API's niet zo vanzelf sprekend zijn bij een basisconfiguratie en je hier echt wel wat aanpassingen aan de configuratie, Codebase of zelfs de Server moet verrichten zodat deze data zich ververst wanneer jij dat wenselijk acht, i.p.v. meerdere dagen achtereenvolgend een statisch karakter aan te nemen.

Hoewel het beheersen van de diverse lagen van Cache op o.a. Browser en Server niveau niet tot mijn expertise behoort, probeer ik sindsdien aan de basis van het ontwikkelen van diverse koppelingen en grotere queries wel na te denken hoe realtime de data moet zijn. Bij een populair evenement kan het bijvoorbeeld van belang zijn dat je iedere 5 minuten de laatste Tweets en andere Social Media data op wil halen op je Social Wall, voor een Social Media feed van je eigen Facebook pagina kan een TTL (time to live, red.) van 6 uur volstaan, terwijl je voor een “meest bekeken videos” koppeling met YouTube een dagelijkse check toereikend kan zijn om het aantal keer dat een video bekeken is “lokaal” op te slaan.

Transients

Om het aantal databaseconnecties op je eigen server, of het aantal API requests te beperken ben ik gebruik gaan maken van de WordPress Transients API. Middels deze standaard beschikbare API kun je grotere queries eenmalig opslaan en het resultaat hiervan in zijn geheel wegschrijven naar één options achtige database entry. Op deze manier hoeft de database niet voor elke keer dat de query wordt aangeroepen een lookup langs de tabellen uit te voeren om het gewenste aantal rijen terug te geven. Het volledige resultaat van jouw query staat immers al compleet met alle relevante rijen voor een bepaalde tijd reeds opgeslagen in de enkele rij die door de Transient is opgeslagen.

Never trust a Transient!

Hoewel het een fijne gedachte is dat bepaalde data kant en klaar staat te wachten in je database of object (zie “Persistant Cache”). Mag je een transient nooit vertrouwen. Een transient is namelijk altijd van tijdelijke aard en hoewel de TTL nog niet verstreken hoeft te zijn, kunnen andere factoren er voor zorgen dat de transient toch al eerder uit de database of het object verdwijnt, denk hierbij aan een Cache Plugin die (al dan niet handmatig) alle Cache opschoont, of het geheugen dat volloopt waardoor de server besluit om de ruimte vrij te geven. Zorg er daarom altijd voor dat je controleert of de transient bestaat met get_transient(); en indien dit niet geval is je deze definieert in het statement met set_transient(); zodat op elk bekend is welke gegevens er moeten worden opgehaald en getoond.

Het code voorbeeld

Hieronder zie je dat er eigenlijk slechts 3 rijen bij komen, 2 voor het ophalen / definiëren van het if statement en 1 voor het wegschrijven van de transient. Alle overige code was sowieso al benodigd om op basis van de ID van de gewenste YouTube video, het aantal “views” op te kunnen halen middels een method die een verbinding maakt naar de YouTube API.

Je controleert eerst met get_transient(); of de transient die je nodig hebt nog bestaat en pas wanneer dit niet het geval is wordt de functionele code die je toch al geschreven hebt uitgevoerd en weggeschreven middels set_transient();. Wanneer de transient bestaat, wordt de get_transient(); toegekend aan de variabele $intViews en bevat deze dezelfde waarde als wanneer de functionele code zou worden uitgevoerd, met de kanttekening dat deze waarde wellicht wat verouderd is, in dit voorbeeld is dat maximaal 24 uur.

/**
 * Get a transient or refetch the data and recreate the transient that's alive for 24h.
 *
 * @author: Simon van der Steen
 * @date: 16-5-2017
 * @url: http://Simon.vdSteen.me/WordPress/cache-praktisch-nut-wordpress-transients-api
 */
$strYouTubeId = 'XXX-XXXXX';
$strTransientId = 'svds_youtube_views_' . $strYouTubeId;

//Check if saved transient exists.
if ( false === ( $intViews = get_transient( $strTransientId ) ) ) {
   //get_youtube_views() is a method to fetch data from the YouTube API.
   $intViews = get_youtube_views( $strYouTubeId );

   //Start of the extra code part which you might not need.
   $intSavedViews = get_post_meta( get_the_ID(), 'youtube_views', true );
   //Check if new value ($intViews) is bigger than the current value ($intSavedViews), else it could indicate that the API call went wrong. If false, the current meta value is re-set in the transient.
   if( $intViews > $intSavedViews ){
      update_post_meta(get_the_ID(), 'youtube_views', $intViews);
   }else{
      $intViews = $intSavedViews;
   }
   //End of the extra code part which you might not need.

   //Save the newly created transient so oncoming users can enjoy the data quicker.
   set_transient( $strTransientId, $intViews, DAY_IN_SECONDS );
}

//Output your transient data.
echo $intViews;

De reden dat ik in bovenstaand voorbeeld bovenop het opslaan van de transient tevens een nieuwe meta wegschrijf, is om deze waarde te gebruiken bij het sorteren van een “meest bekeken” video’s sectie. Dit is voor jouw oplossing wellicht geen vereiste en enkel het opvragen van de transient kan voor jouw casus toereikend zijn.

WP_Cron / save_post en Cache Warming

Een transient kan gebruikt worden om dynamische data “live” op te halen wanneer de bezoeker deze nodig heeft. Maar voor sommige taken kan het handig zijn om de transient alvast op te warmen voor het moment dat de bezoekers komen.

Mocht je data uit een externe API halen en tonen op jouw website, dan kan het zijn dat je deze data en bijbehorende assets in je eigen database wil opslaan zodat de bezoekers er acties op kunnen ondernemen of wellicht zelfs wijzigingen op aan kunnen brengen. Ook kan het zo zijn dat je simpelweg niet afhankelijk wil zijn van de externe partij waarbij data tijdelijk, of definitief verloren kan gaan, of dat je bang bent dat de externe server te kampen heeft met overbelasting. Je kunt deze data ophalen en wegschrijven op het moment dat de gebruiker de pagina bezoekt, maar dit veroorzaakt bij de eerste bezoeker toch een (lichte) vertraging en aangezien het “locking” mechanisme van de transients niet waterdicht is kan het zelfs zo zijn dat meerdere bezoekers die de site vrijwel gelijktijdig raadplegen dezelfde transient opnieuw aan het werk zetten nadat deze is verlopen waardoor één query meerdere keren wordt uitgevoerd en er een zwaardere server load of zelfs dubbele data gegenereerd wordt, dit kan er voor zorgen dat je database vervuild raakt en de gebruikerservaring drastisch verminderd doordat de performance van de server afneemt.

Om deze extra onnodige load en het wegschrijven van overbodige data te voorkomen, kan je er voor kiezen om de data middels een cronjob alvast voor je bezoeker op te halen op vooraf ingestelde tijden. Om dit te bewerkstelligen maak je gebruik van WP_Cron. Aan de methods die je binnen je cronjob uitvoert voeg je tevens een verwijzing naar je transient toe. Zo kun je er óf voor kiezen om de transient enkel te verwijderen met delete_transient();, zodat je garandeert dat de volgende bezoeker de meest recente data ophaalt zodra de cron is afgelopen. Óf je kiest er voor om de Cache alvast een beetje op te warmen door de method die de transient genereert uit te voeren zodra de cron is uitgevoerd. Op deze manier is de transient direct beschikbaar voor de eerst volgende bezoeker. Indien het hier om data gaat die niet heel vaak geraadpleegd zal worden is een cron of het vooraf vullen van de transient wellicht overbodig. Maar wanneer het om data gaat die bijvoorbeeld in je header of op de homepage geladen wordt, wil je misschien niet wachten tot de data een keer wordt opgehaald, maar wil je dat dit warme broodje klaar staat om door je bezoekers verorbert te worden.

Je zal Cache Warming niet ruiken, maar de snelheidswinst kan voor je bezoekers als een warm bad voelen.

In het verlengde daarvan kun je de regie ook wat meer in eigen hand nemen, voor zaken als een Social Wall die je niet vanuit je WordPress Backend beheert is dit wellicht niet ideaal, maar voor content die je vanuit de WordPress Backend genereert kan het handig zijn om deze klaar te zetten in een transient. Zo kan het handig zijn om op de wp_update_nav_menu hook het wp_nav_menu(); object of zelfs de gegenereerde HTML code voor het menu weg te schrijven in een transient. Het volledige menu kan dan worden geladen vanuit 1 database of object rij zonder daarbij alle menu items af in je database af te gaan en in je HTML te verwerken.

Persistant Cache

Hoewel de WordPress Transients API zich bij een standaard server zal beperken tot het gebruik van de database om de transients op te slaan, versterkt het gebruik van een Caching Plugin i.c.m. met een juist geconfigureerde server de manier waarop een transient zich gedraagt. Wanneer bijvoorbeeld gebruikt wordt gemaakt van memcache is de WordPress Transients API in staat dit waar te nemen en wordt gebruik gemaakt van WordPress Object Cache. In plaats van set_transient(); zal de WordPress Transients API gebruik maken van wp_cache_set(); waarbij een object op de server wordt aangemaakt en het verzoek aan de Database Server überhaupt niet hoeft te worden gemaakt. Waarschijnlijk kennen we allemaal de verschillen in snelheid bij het aanroepen van gegevens vanaf een harde schijf (zoals de Database Server zou doen) en direct uit het geheugen (wat het geval is bij het gebruik van bijvoorbeeld memcache. Op deze manier kun je de transients die je na het lezen van deze posts toch al in gaat richten, versterken en een nog grotere optimalisatie van de performance op je server bewerkstelligen.

WP_Query cached Post Meta

Hoewel dit niet direct in een relatie staat tot de WordPress Transients API, ben ik wel tot nieuwe inzichten gekomen aangaande Custom Fields. Ik wil deze bevindingen dan ook met jullie delen zodat jullie je niet blindstaren wanneer je na het gebruik van een transient voor een WP_Query(); object, het aantal gebruikte queries ineens toeneemt.

Het ophalen van een post plaatst alle relevante meta data in de cache.

Lange tijd maakte ik bij het ophalen van posts met een grote hoeveelheid Custom Fields gebruik van get_post_custom(); i.p.v. get_post_meta(); omdat ik dacht dat dit het aantal database requests aanzienlijk zou verminderen. Pas toen ik WP_Query(); objecten als transient ging opslaan, kwam ik er door het gebruik van Query Monitor achter dat bij het initiëren van het WP_Query(); object de Custom Fields behorende bij de zojuist opgehaalde posts (mits update_post_meta_cache  niet op false stond) automatisch in de wp_meta_cache werden weggeschreven en zonder additionele database connectie beschikbaar waren binnen de while loop.

“Cache” all the Things!

Ik hoop natuurlijk dat ik jou net zo heb kunnen enthousiasmeren als dat ik dat bij mijn eerste aanraking met de WordPress Transients API was en je gelijk met Caching en specifiek de WordPress Transients API aan de slag wil gaan. Ik zou jullie nu naast “Het code voorbeeld”, verder aan de hand mee kunnen nemen langs een basis inrichting voor je eerste transient. Echter zijn er al zat mensen met veel meer en onderbouwdere kennis dan dat ik heb. Hieronder heb ik dan ook de “Transient 101” video van gerenommeerde WordPress developer Pippins gedeeld zodat je direct aan de slag kunt.

Uiteraard is er met een juiste inrichting van diverse andere Caching technieken nog een hoop extra winst behalen. Maar je moet ergens beginnen, dus waarom in je toekomstige oplossingen niet alvast de voordelen van het gebruik van transients meenemen.

2 comments

    1. Dit is voor mij lastig te debuggen. Een database error kan allerlei oorzaken hebben, wellicht is het pakket dat je bij je hostingpartij afneemt niet toereikend, als je teveel bezoekers hebt, of teveel data verwerkt en je ruimte vol loopt is een database error vaak een van de eerste signalen waaraan je dit herkend.

      Wat doe je op het moment van de error, onderneem je bepaalde handelingen, of komt de site automatisch terug online de volgende dag? Anders zou het wellicht nog zo kunnen zijn dat je het datalimiet voor die dag hebt verbruikt, al zou een hosting partij dan beter een ander soort melding kunnen laten zien in mijn beleving.

      Als ik alleen al naar jullie homepage kijk denk ik dat een goede cache inrichting de druk al behoorlijk kan ontlasten. Nu worden voor iedere bezoeker per categorie de laatste 7 berichten opgehaald wat alleen al 70 queries kan betekenen als het thema verkeerd is opgebouwd, op piekende momenten is dit wellicht teveel voor het hosting pakket dat je nu afneemt.

      Draai je al iets van een cache plugin zoals W3 Total Cache, WP Rocket of WP Fastest Cache? Deze kunnen op een basis WordPress installatie out-of-the-box best goede performance winsten bewerkstelligen, maar bij maatwerk is het altijd opletten dat de cache op het juiste moment wordt ververst wanneer je (automatisch) nieuwe / dynamische content op het platform plaatst.

Geef een antwoord

Het e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *