SELECT option_value FROM wp_options WHERE option_name = 'wpm_devise_active' : cette même ligne SQL, mot pour mot, apparaissait trois fois dans l’onglet Requêtes de Query Monitor, à chaque chargement de la page d’accueil. Sur un site associatif hébergeant quarante extensions actives, l’audit demandé après une plainte de lenteur a fini par ressembler à une enquête policière : qui interroge quoi, et pourquoi personne ne s’était aperçu que trois autres l’avaient déjà fait ?
L’option en question stockait la devise active du site, une valeur qui ne change presque jamais. Trois extensions distinctes — un module de dons, un widget d’affichage de prix et un connecteur de facturation — avaient chacune été codées indépendamment, par des auteurs différents, sans jamais imaginer qu’elles cohabiteraient sur le même site. Chacune appelait get_option( 'wpm_devise_active' ) à un moment différent du cycle de rendu de la page, ignorant que la valeur avait déjà été récupérée quelques millisecondes plus tôt par une autre.
Le piège du get_option qui semblait pourtant mis en cache
La première réaction a été de vérifier si l’option était autoloadée, ce qui aurait dû limiter la casse : une option avec autoload à yes est chargée une seule fois en mémoire par requête WordPress, via le cache d’objets non persistant. C’était bien le cas. Le problème ne venait donc pas de trois appels à get_option(), qui n’auraient généré qu’une seule requête SQL grâce au cache interne. La vraie cause était plus retorse : l’une des trois extensions appelait directement $wpdb->get_var() avec une requête manuelle, contournant complètement l’API des options et son cache.
Reconstituer le fil avec Query Monitor

L’onglet « Requêtes par composant » de Query Monitor a permis d’identifier précisément quelle extension exécutait quelle requête, grâce à la pile d’appels affichée pour chacune :
- Le module de dons appelait
get_option()normalement, sans souci. - Le widget de prix faisait de même, en bénéficiant donc du cache déjà chargé.
- Le connecteur de facturation, plus ancien, requêtait directement la table
wp_optionsvia$wpdb, pour des raisons de compatibilité avec une version antérieure de WordPress où l’API des options se comportait différemment.
Ce contournement expliquait la troisième requête identique dans les logs, invisible pour qui ne regarde que le nombre total de requêtes sans descendre au niveau du composant responsable.
Un correctif sans toucher aux extensions tierces
Modifier le code d’une extension tierce pour la faire passer par get_option() aurait cassé la compatibilité à chaque mise à jour. La solution retenue a consisté à intercepter la requête directe grâce au filtre query, en réécrivant la clause pour la faire pointer vers une valeur déjà mise en cache dans une variable statique au sein d’un petit plugin maison :
add_filter( 'query', function ( $sql ) {
static $cache = null;
if ( false !== strpos( $sql, "option_name = 'wpm_devise_active'" ) ) {
if ( null === $cache ) {
$cache = get_option( 'wpm_devise_active' );
}
// La requête reste inchangée : on se contente de journaliser
// et de vérifier la cohérence avec le cache local.
}
return $sql;
} );
En pratique, le correctif le plus efficace n’a pas été ce filtre de journalisation, mais un remplacement pur et simple : l’extension de facturation exposait un filtre propre, wpm_billing_get_currency, permettant de lui fournir directement la valeur déjà connue sans qu’elle ait besoin d’interroger la base.
Ce que l’audit a changé dans la méthode
Au-delà du cas précis de la devise, l’exercice a mis en évidence une limite structurelle des extensions WordPress : chacune est pensée comme si elle était seule sur le site. Aucune convention native ne permet à une extension de signaler « j’ai déjà cette donnée, prends-la chez moi » à une autre. L’audit a donc élargi son périmètre à toutes les options lues plus de deux fois par chargement de page, en croisant les résultats de Query Monitor avec un relevé manuel des appels à add_action( 'plugins_loaded' ) de chaque extension.
Un audit qui compte les requêtes sans identifier leur origine ne trouve que des symptômes ; celui qui remonte au composant responsable trouve des causes.
En résumé
Trois requêtes identiques, trois extensions qui s’ignoraient : le problème n’était ni un bug ni une négligence isolée, mais la conséquence naturelle d’un écosystème où chaque module est développé en vase clos. La correction a nécessité de sortir du réflexe « optimiser le cœur » pour aller inspecter, composant par composant, ce que chaque extension demandait réellement à la base de données.