P
imvdmolen.nl
Blog

Hoe ik efficientie win met Laravel's built-in caching mechanisme

In mijn werk draaide ik een performance-audit voor een client waarbij de homepage 4,2 seconden laadde. Het probleem bleek niet de database of de frontend te zijn, maar het ontbreken van intelligente caching. Elke request haalde dezelfde dure berekeningen en API-calls opnieuw op. Na een middag Laravel caching implementeren daalde de laadtijd naar 800 milliseconden. Caching klinkt simpel in theorie, maar de duivel zit in de details van invalidatie, TTL-instellingen en het voorkomen van verouderde data.

Laravel's caching systeem geeft je een solide basis, maar je moet weten welke knoppen je omdraait. Na negentien jaar webdevelopment heb ik geleerd dat slimme caching het verschil maakt tussen een trage applicatie en een snelle. Het gaat niet om alles cachen, maar om de juiste dingen op het juiste moment cachen.

Cache drivers en configuratie

Verschillende projecten vragen om verschillende cache-strategieën. Voor lokale ontwikkeling gebruik ik meestal de file driver omdat die geen extra setup vereist. Op productieservers kies ik vrijwel altijd voor Redis. File caching werkt prima voor kleine applicaties, maar zodra je meerdere processes of servers hebt, loop je tegen problemen aan. Redis daarentegen schaalt moeiteloos mee en biedt geavanceerde features zoals atomic operations.

De driver configureer je in je .env bestand:


CACHE_DRIVER=redis

Basis operaties gaan via Laravel's cache() helper. Je kunt waarden opslaan en ophalen met simpele methodes:

$value = 'Hello, world!';
cache()->put('key', $value);
$cachedValue = cache()->get('key');

Wat ik veel handiger vind is de remember() methode. Deze controleert eerst of de waarde al bestaat, en voert alleen de callback uit als dat niet het geval is. Het voorkomt race conditions en maakt je code leesbaarder:

$users = cache()->remember('active_users', 300, function () {
    return User::active()->get();
});

TTL (Time To Live) stel je in via het tweede argument in seconden. Voor gebruikerslijsten kies ik meestal 5 minuten, voor statische content soms uren of dagen. Het hangt volledig af van hoe vaak je data verandert en hoe kritiek het is dat gebruikers de nieuwste versie zien.

Gestructureerd cachen met tags

Tags zijn een krachtige feature die je helpt gerelateerde cache-items te groeperen. Stel je voor dat je verschillende gebruikersgegevens cachet: profielfoto's, voorkeuren, statistieken. Zonder tags moet je elk item afzonderlijk invalideren wanneer een gebruiker zijn gegevens wijzigt.

Cache::tags('users')->put('user_1', 'John Doe');
Cache::tags('users')->put('user_preferences_1', ['theme' => 'dark']);
Cache::tags('users')->put('user_stats_1', ['posts' => 42]);

Zodra gebruikersdata verandert, flush je de hele groep in één actie:

Cache::tags('users')->flush();

Een belangrijk detail: tags werken alleen met Redis en Memcached. File en database drivers ondersteunen deze functionaliteit niet. Probeer je tags te gebruiken met de file driver, dan krijg je runtime errors. Controleer dus je cache configuratie voordat je tags implementeert.

Externe API's en dure queries cachen

Database queries die joins maken over meerdere tabellen of complexe aggregaties uitvoeren, zijn perfecte kandidaten voor caching. Hetzelfde geldt voor externe API-calls. Waarom zou je elke request opnieuw een lijst landen ophalen van een externe service als die data maandenlang hetzelfde blijft?

$countries = cache()->remember('api_countries', 3600, function () {
    return Http::get('https://api.example.com/countries')->json();
});

Een uur TTL voor relatief statische data zoals landen, valuta of configuratie-instellingen is meestal prima. Externe API's hebben vaak rate limits, dus caching voorkomt ook dat je tegen die limieten aanloopt tijdens piekverkeer.

Database queries cache ik vrijwel nooit rechtstreeks in controllers. Ik bouw altijd een service laag die de caching afhandelt. Dat houdt je controllers schoon en maakt het gemakkelijk om cache-strategieën later aan te passen:

class UserService
{
    public function getActiveUsers()
    {
        return cache()->remember('active_users', 600, function () {
            return User::where('active', true)
                      ->with('profile')
                      ->orderBy('last_login_at', 'desc')
                      ->get();
        });
    }
}

Deze aanpak geeft je ook betere testbaarheid. Je kunt de service mocken zonder je zorgen te maken over cache-gedrag tijdens unit tests.

Cache problemen diagnosticeren

Cache-invalidatie staat terecht bekend als een van de moeilijkste problemen in software development. Verouderde data die plotseling opduikt, inconsistenties die alleen in productie voorkomen, en cache-misses die performance om zeep helpen. Deze problemen zijn frustrerend omdat ze vaak pas opduiken onder load of na een deployment.

Mijn eerste stap bij cache-gerelateerde bugs is altijd een volledige cache clear:

php artisan cache:clear

Lost dit het probleem op, dan weet je dat het om verouderde data ging. Komt het probleem terug, dan ligt het aan je invalidatie-logica of TTL-instellingen.

Voor diepere analyse plaats ik debug-statements in mijn cache-calls. Met een simpele logger() call zie je precies wanneer cache-hits en misses plaatsvinden:

$cachedValue = cache()->get('expensive_calculation');
if ($cachedValue) {
    logger('Cache hit for expensive_calculation');
} else {
    logger('Cache miss for expensive_calculation - executing callback');
}

Laravel Debugbar toont ook gedetailleerde cache-statistieken per request. Aantal hits, misses, en welke keys werden aangeraakt. Dit geeft je inzicht in cache-efficiëntie zonder je productie logs vol te spammen met debug-informatie.

Performance monitoring tools zoals Laravel Telescope registreren cache-operaties automatisch. Vooral handig om patronen te zien over langere periodes. Misschien zie je dat bepaalde cache-items nooit gebruikt worden, of juist veel vaker expiren dan verwacht.

Mijn ervaring leert dat over-caching net zo problematisch kan zijn als onder-caching. Cache alleen wat aantoonbaar duur is: queries die meer dan een paar honderd milliseconden duren, externe API-calls, of complexe berekeningen. Stel altijd een realistische TTL in gebaseerd op hoe vaak je data daadwerkelijk verandert. Een gebruikersprofiel hoeft niet elke seconde geüpdatet te worden, maar een winkelwagentje wel. Het gaat om het vinden van de juiste balans tussen performance en data-freshness.