40 000 apprenants inscrits, répartis en quatre rôles distincts (apprenant, tuteur, formateur, administrateur pédagogique), et un bloc de tableau de bord affiché sur la page d’accueil de chaque espace personnel : voilà le contexte d’une plateforme de formation à distance qui commençait à montrer des signes de ralentissement aux heures de forte affluence, en particulier le lundi matin au moment où la majorité des apprenants se connectaient pour consulter leur progression de la semaine.
Le bloc en cause calcule, à chaque affichage, un résumé personnalisé : modules en cours, prochaine échéance, classement au sein du groupe. Ce calcul mobilise plusieurs requêtes SQL relativement coûteuses, exécutées pour chacun des milliers d’apprenants connectés simultanément le lundi matin.
Le piège d’une clé de cache par utilisateur
La première tentative de mise en cache, avant l’intervention, utilisait une clé de transient incluant l’identifiant de l’utilisateur connecté : acme_dashboard_' . $user_id. Cette approche est intuitivement logique — chaque apprenant a un contenu différent — mais elle produit en pratique 40 000 entrées de cache distinctes, chacune valide une seule fois par apprenant, ce qui revient presque à ne pas avoir de cache du tout un lundi matin où l’essentiel du trafic est un pic de première connexion de la semaine.
Séparer ce qui varie par rôle de ce qui varie par individu
L’analyse du contenu réel du bloc a révélé qu’une bonne partie de sa structure (libellés, disposition, sections visibles) ne dépend que du rôle de l’utilisateur, pas de son identité précise. Seule une sous-partie — la progression personnelle, le classement — dépend réellement de l’apprenant individuel. La stratégie retenue sépare ces deux couches :

function acme_render_dashboard( $attributes ) {
$user = wp_get_current_user();
$role = acme_role_principal( $user );
// Couche 1 : structure commune au rôle, mise en cache longue durée
$structure = wp_cache_get( 'acme_dashboard_structure_' . $role, 'acme' );
if ( false === $structure ) {
$structure = acme_build_dashboard_structure( $role );
wp_cache_set( 'acme_dashboard_structure_' . $role, $structure, 'acme', HOUR_IN_SECONDS );
}
// Couche 2 : données personnelles, requête directe et légère
$progression = acme_get_progression_apprenant( $user->ID );
return acme_assemble_dashboard( $structure, $progression );
}
Ce découpage change radicalement le nombre d’entrées de cache nécessaires : quatre entrées (une par rôle) au lieu de 40 000, chacune valide une heure, contre une explosion combinatoire d’entrées individuelles quasiment jamais réutilisées. La requête de progression individuelle, elle, reste exécutée à chaque affichage, mais a été optimisée séparément pour rester légère (un index dédié sur la table de progression, une seule requête au lieu de trois).
Choisir le bon objet de cache
La plateforme utilisait déjà Redis comme object cache persistant, via un plugin de cache d’objets compatible. Le choix de wp_cache_set() plutôt que set_transient() pour la couche « structure » n’est pas anodin : avec un object cache persistant correctement configuré, wp_cache_set() évite l’écriture systématique en base de données que produit set_transient() en l’absence d’un tel object cache, ce qui aurait ajouté une charge supplémentaire sur la base MySQL déjà sollicitée par les pics de connexion.
Résultats mesurés
| Indicateur | Avant | Après |
|---|---|---|
| Temps moyen de génération du bloc | 340 ms | 75 ms |
| Nombre d’entrées de cache actives | ≈ 40 000 | 4 |
| Requêtes SQL par affichage | 7 | 2 |
Le gain de 78 % sur le temps de génération correspond au passage de 340 à 75 millisecondes en moyenne, mesuré sur une semaine complète après déploiement, heures de pointe du lundi matin incluses. La charge sur la base de données a également nettement diminué, la structure du tableau de bord n’étant plus recalculée qu’une fois par heure et par rôle, quel que soit le nombre d’apprenants connectés simultanément.
Avant de choisir une clé de cache, je pose toujours la question suivante : est-ce que cette donnée varie par utilisateur, ou par catégorie d’utilisateurs ? La réponse change complètement la stratégie, et se trompe presque toujours dans le sens d’une granularité trop fine.
En résumé
Le problème n’était pas l’absence de cache, mais une granularité de cache mal choisie qui neutralisait presque tout son intérêt. Distinguer ce qui relève du rôle de ce qui relève de l’individu a permis de diviser par plus de quatre le temps de génération du bloc, sans perdre la moindre personnalisation réellement perçue par les apprenants.