Si vous avez déjà vu votre page d’accueil mettre 800 ms “juste” pour afficher une liste d’articles, vous avez probablement un point chaud côté base de données… et un cache mal posé (ou absent). Les transients WordPress sont souvent présentés comme un “cache simple”, mais on peut en faire un cache intelligent : versionné, invalidé au bon moment, et sûr en multisite.
Le problème / Le besoin
Beaucoup de sites WordPress (6.9.4 en avril 2026) finissent par accumuler des requêtes coûteuses : requêtes agrégées pour des “top articles”, calculs de stats, appels à des API externes, rendu de blocs dynamiques, etc. Le symptôme typique : TTFB qui grimpe, CPU qui sature sur des pics de trafic, et des pages qui deviennent “aléatoires” (rapides parfois, lentes souvent).
Ce que vous voulez, c’est éviter de recalculer la même donnée à chaque chargement. Mais vous voulez aussi éviter l’autre extrême : un cache qui ne se vide jamais, qui sert des données périmées, ou qui casse quand vous publiez un article.
À la fin, vous saurez :
- Mettre en cache le résultat d’un calcul (ou d’une requête) avec la Transients API.
- Construire des clés de cache fiables (versionnées, contextuelles, multilingues si besoin).
- Invalider le cache au bon moment (publication d’article, changement de termes, options modifiées).
- Éviter les pièges classiques (autoload, collisions de clés, cache “fantôme” avec object cache).
Résumé rapide
- On code un mini “cache layer” basé sur
get_transient()/set_transient(). - On ajoute une version de cache (bump) pour invalider sans balayer des milliers de clés.
- On gère la concurrence (éviter que 50 requêtes recalculent en même temps).
- On invalide via des hooks (ex :
save_post,deleted_post,edited_terms). - On fournit un shortcode et un rendu utilisable dans Divi 5 / Elementor / Avada.
Quand utiliser cette solution
- Listes “Top / Trending” basées sur des métriques (commentaires, vues, popularité) recalculées souvent.
- Widgets/blocs dynamiques qui font des requêtes complexes (taxonomies multiples, meta_query, agrégations).
- Appels API externes (newsletter, CRM, météo, etc.) avec un TTL raisonnable.
- Pages à fort trafic où chaque requête SQL économisée compte (home, catégories, landing pages).
- Back-office lent (tableaux, metaboxes) : les transients fonctionnent aussi en admin.
Quand ne PAS utiliser cette solution
J’ai souvent vu des transients utilisés comme un “cache universel” et ça finit en dette technique. Évitez si :
- Données strictement personnalisées par utilisateur (ex : “vos commandes”) : préférez un cache par user_id, ou pas de cache si la donnée doit être fraîche.
- Vous avez déjà un cache page complet (CDN + cache HTML agressif) et votre problème est ailleurs (images, JS, requêtes admin-ajax).
- Vous devez faire de l’invalidation fine par objet (ex : cache d’un produit WooCommerce avec dépendances multiples) : un système de cache applicatif plus structuré (ou l’Object Cache + groupes) peut être plus adapté.
- Vous voulez stocker beaucoup de données (gros tableaux) : transients en base peuvent gonfler
wp_options. Dans ce cas, privilégiez un object cache persistant (Redis/Memcached) ou une table dédiée.
Alternatives à considérer :
- Object Cache persistant (Redis/Memcached) + API d’object cache : wp_cache_get() / wp_cache_set().
- Cache HTTP (CDN, reverse proxy) si votre contenu est majoritairement public.
- Optimisation de requêtes (
WP_Query, index meta, suppression de requêtes N+1).
Prérequis / avant de commencer
- WordPress 6.9.4+, PHP 8.1+.
- Un environnement de staging (ou local) pour tester. Ne déployez pas ce genre de snippet “direct en prod” sans filet.
- Un moyen sûr d’ajouter du code :
- idéal : un petit plugin mu-plugin (
wp-content/mu-plugins/) - sinon : un plugin de snippets réputé, ou le thème enfant (pas le thème parent)
- idéal : un petit plugin mu-plugin (
- Si vous avez Redis/Memcached : notez que les transients peuvent être stockés dans l’object cache (comportement normal). Voir : Transients API (Handbook).
Précautions sécurité :
- Si vous exposez un bouton “vider le cache” en admin, utilisez capabilities + nonce.
- Évitez de mettre en cache des données sensibles (tokens, infos personnelles) dans des transients partagés.
L’approche naïve (et pourquoi l’éviter)
Le code que je vois souvent : un transient posé “en dur” avec une clé fixe, et aucune invalidation.
<?php
// Exemple naïf : à éviter
function mon_top_articles_naif() {
$key = 'top_articles'; // clé trop générique (collision + pas de contexte)
$data = get_transient( $key );
if ( false === $data ) {
// Requête potentiellement coûteuse
$q = new WP_Query([
'post_type' => 'post',
'posts_per_page' => 5,
'orderby' => 'comment_count',
'order' => 'DESC',
]);
$data = wp_list_pluck( $q->posts, 'ID' );
// TTL arbitraire : 12h
set_transient( $key, $data, 12 * HOUR_IN_SECONDS );
}
return $data;
}
Ce qui se passe en coulisses :
- La clé
top_articlespeut entrer en collision avec un plugin/thème (oui, ça arrive). - La donnée est la même pour tout le monde, mais elle dépend en réalité :
- du site (multisite),
- de la langue (WPML/Polylang),
- du contexte (catégorie, auteur, type de post),
- du statut (brouillons / privés),
- du rôle (si vous filtrez selon les permissions).
- Aucune invalidation : vous publiez un article, et le “top” reste figé jusqu’au TTL.
- En cas de pic de trafic, si le transient expire, 50 requêtes concurrentes peuvent recalculer en même temps (effet “thundering herd”).
La bonne approche — tutoriel pas à pas
Objectif concret
On va créer un mini-plugin qui expose :
- une fonction
bpcab_get_trending_posts()qui renvoie une liste d’IDs d’articles “tendances” (basée sur commentaires récents, simple mais efficace), - un shortcode
[bpcab_trending_posts]pour l’afficher n’importe où (éditeur, Divi, Elementor, Avada), - une invalidation intelligente via “version bump”,
- un bouton admin pour forcer le flush (optionnel, mais pratique).
Étape 1 — Créer un mu-plugin (recommandé)
Créez le fichier :
wp-content/mu-plugins/bpcab-smart-transients-cache.php
Si le dossier mu-plugins n’existe pas, créez-le. Les mu-plugins se chargent automatiquement, ce qui évite le “j’ai désactivé le plugin par erreur”.
Étape 2 — Définir une stratégie de clé (namespace + version + contexte)
Une bonne clé de transient :
- commence par un préfixe unique (slug),
- inclut une version (invalidation globale rapide),
- inclut le contexte (site, langue, paramètres),
- reste courte (les stockages ont parfois des limites).
Étape 3 — Ajouter une “version de cache” (bump)
Au lieu de supprimer 200 transients un par un, on stocke une version globale dans une option (autoload désactivé), et on l’incrémente quand on veut invalider. C’est une technique très robuste en production.
Étape 4 — Gérer la concurrence (verrou léger)
Quand le transient expire, plusieurs requêtes peuvent recalculer. On pose un “lock” (un transient court) : le premier calcule, les autres servent une valeur stale si disponible, ou attendent très brièvement.
Étape 5 — Invalidation via hooks
On invalide quand ça a du sens :
- publication / mise à jour / suppression d’article,
- changement de termes (catégories, tags) si votre sélection en dépend,
- changement d’options si vous rendez la durée configurable.
Code complet
<?php
/**
* Plugin Name: BPCAB Smart Transients Cache (mu-plugin)
* Description: Cache intelligent via Transients API : clés versionnées, invalidation, verrou anti-concurrence, shortcode d'affichage.
* Version: 1.0.0
* Author: BPCAB
*
* Compatible: WordPress 6.9.4+, PHP 8.1+
*/
defined( 'ABSPATH' ) || exit;
/**
* Retourne la version globale du cache applicatif.
*
* On utilise une option NON autoloadée pour éviter de gonfler la mémoire sur chaque requête.
*/
function bpcab_cache_version_get(): int {
$version = get_option( 'bpcab_cache_version', 1 );
// Sécurise le type : certains sites finissent avec des strings dans les options.
$version = is_numeric( $version ) ? (int) $version : 1;
return max( 1, $version );
}
/**
* Incrémente la version globale du cache applicatif.
* Cela "invalide" toutes les clés qui l'incluent, sans suppression massive.
*/
function bpcab_cache_version_bump(): int {
$current = bpcab_cache_version_get();
$new = $current + 1;
// autoload = false (3e paramètre) : important pour les perfs.
update_option( 'bpcab_cache_version', $new, false );
return $new;
}
/**
* Construit une clé de transient stable et contextualisée.
*
* @param string $group Groupe fonctionnel (ex: 'trending_posts')
* @param array $context Données qui doivent influencer le cache (ex: langue, catégorie, limite)
*/
function bpcab_transient_key( string $group, array $context = [] ): string {
$blog_id = function_exists( 'get_current_blog_id' ) ? (int) get_current_blog_id() : 1;
// Langue : si vous êtes en multilingue, ce petit morceau évite de servir la mauvaise langue.
$locale = function_exists( 'determine_locale' ) ? determine_locale() : get_locale();
$version = bpcab_cache_version_get();
// Normalisation du contexte pour éviter les hash instables.
ksort( $context );
$payload = wp_json_encode(
[
'v' => $version,
'blog' => $blog_id,
'locale' => $locale,
'ctx' => $context,
],
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
);
// Hash court pour garder une clé raisonnable.
$hash = substr( md5( (string) $payload ), 0, 12 );
// Préfixe + groupe + hash.
return 'bpcab_' . sanitize_key( $group ) . '_' . $hash;
}
/**
* Récupère des IDs d'articles "tendance" avec cache intelligent.
*
* Stratégie :
* - On calcule une requête basée sur les commentaires récents (ex: 14 derniers jours).
* - On cache la liste d'IDs pendant un TTL raisonnable.
* - On utilise un lock pour éviter que plusieurs requêtes recalculent en même temps.
*
* @param int $limit Nombre d'articles.
* @param int $days Fenêtre de temps (jours) pour compter les commentaires.
* @param int $ttl Durée de cache en secondes.
* @return int[] Liste d'IDs.
*/
function bpcab_get_trending_posts( int $limit = 5, int $days = 14, int $ttl = 10 * MINUTE_IN_SECONDS ): array {
$limit = max( 1, min( 20, $limit ) );
$days = max( 1, min( 90, $days ) );
$ttl = max( 30, min( DAY_IN_SECONDS, $ttl ) );
$key = bpcab_transient_key(
'trending_posts',
[
'limit' => $limit,
'days' => $days,
]
);
$cached = get_transient( $key );
if ( is_array( $cached ) ) {
// Nettoyage minimal : on force des int.
return array_values( array_filter( array_map( 'intval', $cached ) ) );
}
// Lock anti-concurrence (verrou léger).
$lock_key = $key . '_lock';
$has_lock = get_transient( $lock_key );
// Si un autre processus calcule déjà, on évite de recalculer.
// Sur certains sites, ça réduit drastiquement les pics CPU à l'expiration.
if ( $has_lock ) {
// Pas de stale store dans cet exemple, donc on renvoie une valeur "safe".
// Variante possible : servir un cache plus ancien (stale-while-revalidate).
return [];
}
// On prend le lock pour 20 secondes : suffisant pour calculer.
set_transient( $lock_key, 1, 20 );
global $wpdb;
$since = gmdate( 'Y-m-d H:i:s', time() - ( $days * DAY_IN_SECONDS ) );
// Requête SQL : on compte les commentaires approuvés sur les posts publiés.
// On retourne les IDs triés par volume de commentaires récents.
$sql = $wpdb->prepare(
"
SELECT p.ID
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->comments} c ON c.comment_post_ID = p.ID
WHERE p.post_type = 'post'
AND p.post_status = 'publish'
AND c.comment_approved = '1'
AND c.comment_date_gmt >= %s
GROUP BY p.ID
ORDER BY COUNT(c.comment_ID) DESC, MAX(c.comment_date_gmt) DESC
LIMIT %d
",
$since,
$limit
);
$ids = $wpdb->get_col( $sql );
$ids = is_array( $ids ) ? array_values( array_filter( array_map( 'intval', $ids ) ) ) : [];
// On stocke le résultat.
set_transient( $key, $ids, $ttl );
// On libère le lock.
delete_transient( $lock_key );
return $ids;
}
/**
* Shortcode : [bpcab_trending_posts limit="5" days="14" ttl="600" title="Tendances"]
*/
function bpcab_trending_posts_shortcode( $atts ): string {
$atts = shortcode_atts(
[
'limit' => 5,
'days' => 14,
'ttl' => 600,
'title' => '',
],
(array) $atts,
'bpcab_trending_posts'
);
$limit = (int) $atts['limit'];
$days = (int) $atts['days'];
$ttl = (int) $atts['ttl'];
$title = sanitize_text_field( (string) $atts['title'] );
$ids = bpcab_get_trending_posts( $limit, $days, $ttl );
if ( empty( $ids ) ) {
return '';
}
// Récupération des posts dans l'ordre des IDs.
$q = new WP_Query(
[
'post_type' => 'post',
'post_status' => 'publish',
'post__in' => $ids,
'orderby' => 'post__in',
'posts_per_page' => count( $ids ),
'no_found_rows' => true,
'ignore_sticky_posts' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
]
);
if ( ! $q->have_posts() ) {
return '';
}
ob_start();
?>
<div class="bpcab-trending-posts">
<?php if ( $title !== '' ) : ?>
<p><strong><?php echo esc_html( $title ); ?></strong></p>
<?php endif; ?>
<ul>
<?php while ( $q->have_posts() ) : $q->the_post(); ?>
<li>
<a href="<?php echo esc_url( get_permalink() ); ?>">
<?php echo esc_html( get_the_title() ); ?>
</a>
</li>
<?php endwhile; ?>
</ul>
</div>
<?php
wp_reset_postdata();
return (string) ob_get_clean();
}
add_shortcode( 'bpcab_trending_posts', 'bpcab_trending_posts_shortcode' );
/**
* Invalidation : on "bump" la version quand des contenus changent.
*
* Note : vous pouvez affiner (ex: uniquement si post_status = publish).
*/
function bpcab_invalidate_cache_on_post_change( int $post_id ): void {
// Évite les autosaves/révisions.
if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
return;
}
$post = get_post( $post_id );
if ( ! $post || $post->post_type !== 'post' ) {
return;
}
// Si un post est publié/mis à jour/supprimé, le "trending" peut changer.
bpcab_cache_version_bump();
}
add_action( 'save_post', 'bpcab_invalidate_cache_on_post_change', 20 );
add_action( 'deleted_post', 'bpcab_invalidate_cache_on_post_change', 20 );
/**
* Invalidation sur changement de termes (catégories/tags) :
* utile si vous adaptez le cache par catégorie/tag.
*/
function bpcab_invalidate_cache_on_terms_change(): void {
bpcab_cache_version_bump();
}
add_action( 'edited_terms', 'bpcab_invalidate_cache_on_terms_change', 20 );
add_action( 'created_term', 'bpcab_invalidate_cache_on_terms_change', 20 );
add_action( 'delete_term', 'bpcab_invalidate_cache_on_terms_change', 20 );
/**
* (Optionnel) Outil admin : bouton pour forcer l'invalidation.
* Sécurisé via capability + nonce.
*/
function bpcab_cache_tools_menu(): void {
add_management_page(
'Cache BPCAB',
'Cache BPCAB',
'manage_options',
'bpcab-cache-tools',
'bpcab_cache_tools_page'
);
}
add_action( 'admin_menu', 'bpcab_cache_tools_menu' );
function bpcab_cache_tools_page(): void {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Accès refusé.', 'default' ) );
}
// Traitement du formulaire.
if ( isset( $_POST['bpcab_cache_flush'] ) ) {
check_admin_referer( 'bpcab_cache_flush_action', 'bpcab_cache_flush_nonce' );
$new = bpcab_cache_version_bump();
echo '<div class="notice notice-success"><p>' . esc_html( 'Cache invalidé. Nouvelle version : ' . $new ) . '</p></div>';
}
$current = bpcab_cache_version_get();
?>
<div class="wrap">
<h1>Cache BPCAB</h1>
<p>Version actuelle du cache : <strong><?php echo esc_html( (string) $current ); ?></strong></p>
<form method="post">
<?php wp_nonce_field( 'bpcab_cache_flush_action', 'bpcab_cache_flush_nonce' ); ?>
<p>
<button type="submit" name="bpcab_cache_flush" class="button button-primary">Invalider le cache maintenant</button>
</p>
</form>
</div>
<?php
}
Explication du code
Lecture simple (ce que ça fait)
- Une option
bpcab_cache_versionsert de “compteur”. - Chaque clé de transient inclut ce compteur : quand il change, les anciennes clés deviennent invisibles.
- La fonction
bpcab_get_trending_posts():- tente de lire le cache,
- sinon prend un verrou court, calcule, stocke, libère le verrou.
- Des hooks incrémentent la version quand un post/terme change.
- Un shortcode rend la liste.
Détails techniques (ce qui compte en prod)
Transients : get_transient() renvoie false si absent/expiré. Avec un object cache persistant, la donnée peut être stockée en mémoire plutôt qu’en base. C’est transparent côté code. Doc officielle : get_transient() et set_transient().
Version bump : au lieu de supprimer des transients (coûteux et parfois impossible sans connaître toutes les clés), on change juste la version. Les anciennes entrées expireront naturellement. Sur des sites à fort trafic, c’est souvent plus stable qu’un “flush massif”.
Option non autoloadée : update_option(..., false) évite que l’option soit chargée à chaque requête via l’autoload de wp_options. J’ai déjà vu des sites ralentis simplement parce qu’ils avaient trop d’options autoloadées. Référence : update_option().
Lock : on utilise un transient _lock de 20s. Ce n’est pas un mutex parfait, mais c’est suffisant pour réduire la casse lors d’une expiration simultanée. Si vous voulez un verrou plus robuste, vous pouvez basculer sur un lock via object cache (groupes) ou une table dédiée, mais ça complique vite.
SQL via $wpdb : ici, on fait une agrégation (COUNT) sur les commentaires récents. C’est typiquement le genre de requête que vous ne voulez pas lancer sur chaque page vue. Référence : $wpdb.
Sanitization / escaping :
- Entrées shortcode :
sanitize_text_field()+ casts enint. - Sorties HTML :
esc_html()etesc_url(). - Admin flush :
current_user_can()+check_admin_referer()(nonce).
Variantes et cas d’usage
Variante 1 — Cache par catégorie (clé contextualisée)
Si vous voulez un “trending” par catégorie, ajoutez cat_id au contexte et filtrez la requête. Le point important : la clé doit inclure la catégorie, sinon vous servirez la mauvaise liste.
<?php
function bpcab_get_trending_posts_by_category( int $cat_id, int $limit = 5 ): array {
$cat_id = max( 1, $cat_id );
$key = bpcab_transient_key(
'trending_posts_cat',
[
'cat_id' => $cat_id,
'limit' => $limit,
]
);
$cached = get_transient( $key );
if ( is_array( $cached ) ) {
return array_values( array_filter( array_map( 'intval', $cached ) ) );
}
$q = new WP_Query(
[
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => $limit,
'cat' => $cat_id,
'orderby' => 'comment_count',
'order' => 'DESC',
'no_found_rows' => true,
'ignore_sticky_posts' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
]
);
$ids = wp_list_pluck( $q->posts, 'ID' );
$ids = array_values( array_filter( array_map( 'intval', $ids ) ) );
set_transient( $key, $ids, 10 * MINUTE_IN_SECONDS );
return $ids;
}
Variante 2 — Stale-while-revalidate (servir l’ancien pendant recalcul)
Sur des sites à fort trafic, renvoyer [] pendant le lock est parfois trop brutal. Une stratégie simple : stocker aussi un cache “stale” plus long. Quand le cache principal expire, vous servez le stale et vous recalculerez en arrière-plan (ou sur la requête courante, mais sans bloquer tout le monde).
Je ne vous colle pas ici un système de background processing complet (ça mérite un article dédié), mais l’idée est :
- transient principal TTL court (ex : 10 min),
- transient stale TTL long (ex : 24h),
- si lock actif : servir stale.
Variante 3 — Cache d’API externe (avec gestion d’erreur)
Quand l’API tombe, vous ne voulez pas écraser un bon cache avec une erreur. Gardez l’ancien si le fetch échoue.
<?php
function bpcab_get_remote_data_cached(): array {
$key = bpcab_transient_key( 'remote_api', [ 'endpoint' => 'example' ] );
$cached = get_transient( $key );
if ( is_array( $cached ) ) {
return $cached;
}
$response = wp_remote_get( 'https://example.com/api/data', [ 'timeout' => 5 ] );
if ( is_wp_error( $response ) ) {
// Ne remplacez pas un cache potentiellement valide par une erreur.
return [];
}
$code = (int) wp_remote_retrieve_response_code( $response );
if ( $code < 200 || $code >= 300 ) {
return [];
}
$body = (string) wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
if ( ! is_array( $data ) ) {
return [];
}
set_transient( $key, $data, 15 * MINUTE_IN_SECONDS );
return $data;
}
Compatibilité Divi 5 / Elementor / Avada
Divi 5
Divi sait afficher des shortcodes dans la plupart des modules texte/code. Le plus fiable :
- Ajoutez un module “Code” ou “Texte”.
- Collez :
[bpcab_trending_posts limit="5" days="14" ttl="600" title="Tendances"]
Piège courant : activer un cache statique agressif dans Divi/Performance et oublier de purger. Si vous testez des variations de TTL, purge Divi + cache serveur.
Elementor
Utilisez le widget “Shortcode”. Même shortcode, mêmes attributs.
Attention : si vous utilisez un plugin de cache qui minifie/concatène et retarde le rendu, vous pouvez croire que le cache ne change pas. Testez en navigation privée + purge cache plugin.
Avada (Fusion Builder)
Avada a un élément “Shortcode” / “Code Block” selon versions. Collez le shortcode tel quel.
J’ai déjà vu Avada servir des fragments mis en cache côté builder. Si vous modifiez le code PHP, videz aussi les caches Avada (Performance / Cache) en plus des transients.
Vérifications après mise en place
- Affichez le shortcode sur une page de test.
- Chargez la page 3 fois :
- 1ère fois : calcul (plus lent),
- 2e/3e : cache (plus rapide).
- Vérifiez dans la base :
- Sans object cache persistant, les transients sont stockés dans
wp_options(clés_transient_*). - Avec Redis/Memcached, ils peuvent ne pas apparaître en base (normal).
- Sans object cache persistant, les transients sont stockés dans
- Publiez un nouvel article, ou approuvez un commentaire, puis rechargez : la version de cache doit changer (page Outils → Cache BPCAB) et la liste peut évoluer.
Tableau de diagnostic rapide
| Symptôme | Cause probable | Vérification | Solution |
|---|---|---|---|
| La liste ne change jamais | Invalidation non déclenchée | Regardez la version dans Outils → Cache BPCAB après publication | Vérifiez les hooks, la priorité, et que le code est bien chargé |
| La liste est souvent vide | Lock actif + pas de stale fallback | Ajoutez un log temporaire sur $has_lock |
Implémentez stale-while-revalidate ou augmentez TTL/lock |
| Pas de gain de perf | Cache page/serveur masque l’effet, ou requête ailleurs | Mesurez TTFB / Query Monitor | Profilez, identifiez le vrai point chaud |
| Erreur 500 après ajout du code | Syntaxe PHP (point-virgule, parenthèse) | Consultez debug.log / logs serveur |
Corrigez, redeployez, évitez l’édition en prod |
Si ça ne marche pas
1) Confirmez que le code est chargé
- Si c’est un mu-plugin : vérifiez le chemin exact
wp-content/mu-plugins/.... - Si c’est un thème enfant : vérifiez que vous n’avez pas mis le code dans le thème parent (mise à jour = perdu).
- Ajoutez temporairement un
error_log('BPCAB cache loaded');et vérifiez les logs.
2) Activez le debug proprement
Dans wp-config.php (staging), utilisez :
<?php
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
Ensuite, consultez wp-content/debug.log.
3) Vérifiez les conflits de cache
- Purge du cache plugin (LiteSpeed Cache, WP Rocket, etc.).
- Si vous avez Redis : vérifiez que l’object cache n’est pas en erreur (plugin Redis Object Cache).
- Videz le cache navigateur si vous testez en front sur une page très cachée.
4) Vérifiez les hooks d’invalidation
Erreur fréquente : utiliser publish_post uniquement, puis se demander pourquoi une mise à jour ne flush pas. Ici on utilise save_post + garde-fous autosave/révision.
5) Vérifiez la version PHP
Si vous êtes en PHP 7.4/8.0 par inadvertance, vous pouvez avoir des comportements inattendus (types, etc.). Le code cible PHP 8.1+. Référence : PHP 8.1 release notes.
Pièges et erreurs courantes
| Erreur | Cause | Solution |
|---|---|---|
| Code copié dans le mauvais fichier | Ajout dans le thème parent ou un plugin non chargé | Préférez mu-plugin ou plugin dédié, vérifiez le chargement |
| Parse error: syntax error, unexpected … | Point-virgule/parenthèse manquant | Relisez la ligne indiquée dans les logs, utilisez un éditeur avec lint PHP |
| La clé de transient “se marche dessus” | Clé trop générique, pas de contexte | Namespace + hash + version + paramètres (comme dans bpcab_transient_key()) |
| Le cache ne s’invalide pas après modification | Hook inadapté ou priorité trop basse | Utilisez save_post (avec garde-fous) et vérifiez la priorité |
| Résultats incohérents en multisite | Clé sans blog_id | Incluez get_current_blog_id() dans la clé |
| Données dans la mauvaise langue | Clé sans locale | Incluez determine_locale() ou un code langue WPML/Polylang |
| Le cache “gonfle” la base | Trop de transients, TTL long, données volumineuses | Réduisez la cardinalité des clés, TTL, ou passez à Redis/Memcached |
| Confusion actions/filtres | Utiliser add_filter au lieu de add_action (ou inverse) |
Pour invalidation : actions. Pour modifier une valeur : filtres |
| Tester en production sans sauvegarde | Snippet ajouté “à la volée” | Staging + sauvegarde + plan de rollback |
Conseils sécurité, performance et maintenance
Sécurité
- Ne mettez pas en cache des données sensibles partagées (tokens API, données personnelles) sans chiffrement et sans cloisonnement par utilisateur.
- Si vous exposez une action de purge : capability stricte + nonce (comme dans la page Outils).
Performance
- TTL réaliste : 10 minutes pour un “trending” est souvent un bon compromis. 12 heures est souvent trop long si vous publiez fréquemment.
- Réduisez la cardinalité : si votre clé inclut trop de paramètres (ex : par utilisateur, par device, par referrer), vous allez créer un cache inutilement fragmenté.
- Préférez des payloads légers : stocker des IDs puis requêter les posts est souvent plus stable que stocker des objets complets.
- Object cache persistant : si votre site a du trafic, Redis/Memcached change la donne. Les transients deviennent beaucoup plus efficaces.
Maintenance
- Documentez vos groupes de cache (
trending_posts,remote_api, etc.). - Gardez une méthode d’invalidation globale (version bump) : c’est votre bouton “panic”.
- Surveillez
wp_optionssi vous n’avez pas d’object cache persistant (taille, autoload).
Ressources
- Transients API (WordPress Developer Handbook)
- get_transient()
- set_transient()
- delete_transient()
- wp_cache_get() (Object Cache API)
- Classe wpdb
- Code source WordPress (wordpress-develop)
- WordPress Core Trac
- PHP: json_decode()
FAQ
Les transients sont-ils “toujours” stockés en base de données ?
Non. Sans object cache persistant, WordPress stocke les transients en base (options). Avec Redis/Memcached (object cache drop-in), ils sont généralement stockés en mémoire. Votre code ne change pas, mais vos outils d’inspection oui.
Quelle différence entre Transients API et Object Cache API ?
Les transients ont une notion de TTL “standard” et un stockage géré par WordPress. L’Object Cache API (wp_cache_*) est plus bas niveau et dépend fortement de votre backend (et des groupes). Si vous avez Redis/Memcached, l’Object Cache est souvent plus flexible, mais les transients restent très pratiques pour du cache applicatif simple.
Pourquoi utiliser une “version” plutôt que delete_transient() ?
Parce que vous ne connaissez pas toujours toutes les clés à supprimer (surtout si elles incluent un hash). Bumper la version invalide tout d’un coup, sans scan. Les anciennes entrées expirent ensuite naturellement.
Est-ce que ça peut remplir ma table wp_options ?
Oui si vous créez énormément de clés (cache trop fragmenté) ou si vous stockez de gros payloads avec des TTL longs. Sur un site sans object cache, surveillez le volume et évitez de multiplier les variantes inutiles.
Pourquoi ma liste “tendance” est vide après expiration ?
Dans ce code, si un lock est déjà pris, on renvoie []. C’est volontaire pour rester simple. Si vous voulez une UX stable, implémentez une variante stale-while-revalidate (servir une valeur précédente pendant le recalcul).
Quel TTL choisir ?
Pour des contenus éditoriaux : 5 à 15 minutes fonctionne bien. Pour une API externe instable : 10 à 30 minutes. Pour des données quasi statiques : plusieurs heures, mais avec une invalidation manuelle disponible.
Est-ce compatible multisite ?
Oui, la clé inclut get_current_blog_id(). Sans ça, vous pouvez servir le cache d’un site à un autre (bug très pénible à diagnostiquer).
Et avec WPML/Polylang ?
La clé inclut determine_locale(). Selon votre setup, vous voudrez parfois utiliser un code langue spécifique (ex : filtre WPML) pour être parfaitement aligné avec votre logique de contenu.
Pourquoi ne pas stocker directement les titres/URLs dans le transient ?
Parce que les titres changent, les permaliens peuvent être régénérés, et vous risquez de servir des données périmées plus longtemps. Stocker des IDs est plus résilient, et WordPress gère ensuite le rendu à jour.
Comment tester proprement les gains ?
Mesurez avant/après avec un outil de profiling (Query Monitor, ou APM). Regardez le nombre de requêtes SQL et le temps total. Faites vos tests sans cache page (ou en le purgeant entre essais), sinon vous mesurez surtout le cache HTML.
Ce code peut-il casser un site ?
Comme tout code PHP, oui : une erreur de syntaxe suffit à déclencher une erreur 500. Testez sur staging, et gardez un accès FTP/SSH pour retirer le fichier mu-plugin en cas de problème.