Comment servir un même bloc de progression de cours à 60 000 apprenants actifs simultanément, sans que chacun d’eux déclenche sa propre requête de calcul de progression individuelle ? C’est la question posée par une plateforme de formation en ligne dont le bloc formation/progression-cours, affiché sur chaque page de leçon, était devenu le principal goulot d’étranglement du site aux heures de forte affluence.
Ce retour d’expérience porte sur la stratégie de cache mise en place pour ce bloc précis. Il ne couvre pas le moteur de cours lui-même, déjà en place et hors du périmètre de cette intervention, ni la gestion des paiements d’inscription aux formations.
Le diagnostic initial
Le bloc calculait, à chaque affichage, la progression de l’apprenant dans le cours en cours (pourcentage de leçons terminées, prochaine leçon recommandée) via plusieurs requêtes SQL croisant les tables de progression et le contenu du cours. Ce calcul, individuel par nature puisqu’il dépend de l’apprenant connecté, semblait à première vue impossible à mettre en cache : chaque apprenant a sa propre progression.
La clé qui a débloqué le problème
La percée est venue d’une observation simple : au sein d’un même cours, les apprenants se répartissent en un nombre limité d’états de progression distincts (par exemple, onze paliers possibles sur un cours de dix leçons), pas soixante mille états uniques. La clé de cache n’a donc pas été construite sur l’identifiant individuel de l’apprenant, mais sur la combinaison du rôle utilisateur, de l’identifiant du cours, et du palier de progression atteint :
function formation_construire_cle_cache( $utilisateur_id, $cours_id ) {
$palier = formation_calculer_palier_progression( $utilisateur_id, $cours_id );
$role = formation_role_pertinent_pour_cache( $utilisateur_id );
return sprintf( 'progression_%s_%d_%d', $role, $cours_id, $palier );
}

Le rendu HTML associé à un palier de progression donné, pour un rôle donné (apprenant standard, apprenant en rattrapage, tuteur), est strictement identique d’un apprenant à l’autre : seul le palier change l’affichage, jamais l’identité individuelle. Cette clé composée a permis de réduire drastiquement le nombre d’entrées de cache distinctes, malgré le nombre élevé d’apprenants réels.
Le mécanisme d’invalidation ciblée
Une purge globale du cache objet à chaque changement de progression aurait annulé tout le bénéfice de cette approche, en invalidant des milliers d’entrées à chaque validation de leçon par un apprenant. L’invalidation cible désormais précisément l’entrée correspondant à l’ancien palier de l’apprenant concerné, jamais l’ensemble du cache du cours :
function formation_invalider_cache_progression( $utilisateur_id, $cours_id, $ancien_palier ) {
$role = formation_role_pertinent_pour_cache( $utilisateur_id );
$cle = sprintf( 'progression_%s_%d_%d', $role, $cours_id, $ancien_palier );
wp_cache_delete( $cle, 'formation_progression' );
}
add_action( 'formation_leçon_validee', 'formation_invalider_cache_progression', 10, 3 );
Le résultat mesuré
| Mesure | Avant cache croisé | Après cache croisé |
|---|---|---|
| Temps de réponse moyen du bloc | 420 ms | 70 ms |
| Requêtes SQL par affichage | 6 à 9 | 0, hors invalidation |
| Entrées de cache distinctes pour 60 000 apprenants | Non applicable | Quelques centaines par cours |
La limite de cette approche
Cette stratégie fonctionne parce que le rendu du bloc ne dépend que d’un nombre restreint de variables (rôle, cours, palier), pas de données réellement uniques par apprenant comme un nom affiché ou un commentaire personnel. Un bloc affichant des données véritablement individuelles, comme un message de bienvenue nominatif, ne pourrait pas bénéficier du même regroupement et demanderait une stratégie de cache différente, probablement plus fine et moins efficace en taux de réussite.
Le secret d’un bon cache n’est presque jamais la durée de rétention choisie : c’est la capacité à repérer que des milliers d’utilisateurs différents partagent, en réalité, un nombre restreint d’états d’affichage identiques.
En résumé
Regrouper la clé de cache autour du rôle, du cours et du palier de progression plutôt que de l’identifiant individuel de l’apprenant a divisé par six le temps de réponse du bloc concerné, tout en gardant une invalidation ciblée et précise. Cette approche reste transposable à tout bloc affichant une donnée qui dépend de l’utilisateur mais se regroupe naturellement en un nombre limité d’états distincts.