# Un thème bloc pour un média à 300 000 articles : purge et cache de patterns

> Retour chiffré sur la purge de cache par catégorie et l'optimisation du rendu de patterns partagés sur un site de presse à fort volume.

- Auteur : WordPress Développement
- Publié le : 2023-01-12
- Mis à jour le : 2023-01-12
- Catégorie : Éditeur de site (FSE)
- URL : https://www.wpmoderne.fr/fse/cache-patterns-media-300000-articles/

## L’essentiel

- Purge par catégorie plutôt que purge globale du site
- Rendu de pattern mis en cache avec clé de version
- Temps de génération divisé sur les pages d'archive

`wp cache flush` sur un site de 300 000 articles : cette commande, exécutée par réflexe après chaque publication, a mis un serveur à genoux pendant plus d'un an avant qu'on ne comprenne pourquoi. Le thème bloc livré à ce média régional générait ses pages d'archive à partir de patterns partagés entre des dizaines de catégories, et chaque purge globale forçait la reconstruction simultanée de milliers de fragments de cache.

Ce retour d'expérience décrit la stratégie de purge ciblée par catégorie mise en place, ainsi que l'optimisation du rendu des patterns partagés qui a suivi. La question du CDN d'images et celle de la monétisation publicitaire ne sont pas traitées ici : ce sont des chantiers séparés, menés par d'autres prestataires sur ce projet.

## Le problème : une purge qui coûte plus cher que le gain

Le thème bloc utilisait un unique pattern `archive-header` partagé par les 40 catégories du site, incluant un bloc de requête affichant les articles les plus lus de la rubrique. Chaque nouvel article déclenchait, via un hook mal calibré sur `save_post`, un appel à `wp_cache_flush()` qui vidait l'intégralité de l'object cache Redis, y compris les objets sans rapport avec la publication en cours.

Sur 300 000 articles et un trafic soutenu, cette purge globale provoquait un pic de charge à chaque publication, aux heures où la rédaction publie le plus : entre 7 h et 9 h. Le serveur régénérait alors, en quelques secondes, l'ensemble des pages d'archive du site simultanément sollicitées par les visiteurs.

## La solution : purger par groupe de cache

La première étape a consisté à remplacer le flush global par des groupes de cache dédiés, un par catégorie, en s'appuyant sur `wp_cache_add_non_persistent_groups()` et une clé de cache composée de l'identifiant de catégorie et d'un horodatage de dernière modification stocké en option.

> L'essentiel à retenir : Purge par catégorie plutôt que purge globale du site ; Rendu de pattern mis en cache avec clé de version ; Temps de génération divisé sur les pages d'archive

```
function wpm_bump_category_cache_version( $post_id ) {
    $categories = get_the_category( $post_id );
    foreach ( $categories as $category ) {
        $key = 'wpm_cat_cache_version_' . $category->term_id;
        update_option( $key, time(), false );
    }
}
add_action( 'save_post_post', 'wpm_bump_category_cache_version' );

function wpm_get_category_cache_key( $term_id ) {
    $version = get_option( 'wpm_cat_cache_version_' . $term_id, '0' );
    return 'archive_header_' . $term_id . '_' . $version;
}
```

Cette clé versionnée est ensuite utilisée pour envelopper le rendu du pattern `archive-header` dans un appel à `wp_cache_get()` / `wp_cache_set()`, avec une expiration longue puisque l'invalidation se fait désormais par changement de version, non par durée.

## Optimiser le rendu du pattern lui-même

La purge ciblée a réglé le pic de charge lié aux publications, mais le temps de génération d'une page d'archive restait élevé : le bloc de requête « articles les plus lus » exécutait une requête `WP_Query` triée par un champ de compteur de vues à chaque affichage, y compris en cas de cache manquant sur une seule catégorie parmi quarante.

- Le compteur de vues, mis à jour à chaque visite, a été déplacé vers une agrégation asynchrone via une tâche `wp_cron` horaire plutôt qu'un `UPDATE` synchrone sur chaque page vue.
- La requête de tri a été indexée en base via `meta_query` avec une clé numérique dédiée, évitant un tri en mémoire côté MySQL sur des dizaines de milliers de lignes.
- Le rendu final du pattern a été mis en cache de fragment séparément de la requête, avec sa propre clé de version, pour permettre de rafraîchir les vues sans invalider tout l'en-tête d'archive.

## Les chiffres après trois mois

| Indicateur | Avant | Après |
| --- | --- | --- |
| Temps de rendu moyen d'une archive | 1,8 s | 0,3 s |
| Purges de cache par jour | ~450 | ~40 |
| Pic CPU aux heures de publication | 92 % | 34 % |

Le gain le plus visible pour la rédaction : la publication d'un article n'entraîne plus de ralentissement perceptible sur le reste du site, ce qui a permis de supprimer une contrainte opérationnelle absurde consistant à éviter de publier plusieurs articles à la même minute.

### Un piège évité de justesse

Le versionnement par option a failli créer un nouveau problème : sans limite, la table `wp_options` aurait accumulé une entrée orpheline par catégorie supprimée. Un nettoyage sur le hook `delete_term` a été ajouté pour éviter cette dérive, un détail facile à négliger dans ce genre de dispositif.

## Notre verdict

La leçon principale de ce projet tient en une phrase : sur un site à fort volume, la granularité de l'invalidation de cache compte davantage que sa durée. Un cache généreux mais purgé finement bat systématiquement un cache court mais purgé globalement. La contrepartie est une complexité de code plus élevée, qu'il faut documenter soigneusement pour l'équipe de rédaction technique qui reprendra ce thème bloc dans quelques années.
