De 393 à 85 requêtes SQL : optimiser une page WordPress avec Query Monitor et ACF
Mis à jour en 2026
Introduction
Une page Tarifs WordPress bien conçue côté éditorial peut devenir un cauchemar côté SQL. C’est exactement ce que j’ai constaté sur un projet récent : une page dense, administrée entièrement via ACF Pro sur une options page, qui générait à chaque chargement 393 requêtes SQL — dont 331 uniquement liées à la table wp_options.
Ni un problème d’hébergement, ni un plugin mal codé. Le vrai coupable : une architecture de lecture front parfaitement cohérente avec ACF, mais inadaptée à la densité du contenu.
Ce cas illustre pourquoi Query Monitor est indispensable dans une stack WordPress sérieuse, et pourquoi l’optimisation des get_field() ne suffit parfois pas. La bonne réponse est parfois architecturale.
Le contexte : une page Tarifs riche et administrable
La page en question agrégait plusieurs blocs de contenu :
- 3 offres fixes avec tarifs, détails et appels à l’action
- un estimateur pour les grands comptes
- un tableau comparatif de fonctionnalités
- des settings globaux (mentions légales, libellés, mise en avant)
Côté back-office, tout était géré via ACF Pro sur une options page — une approche confortable, qui évite de toucher au code pour mettre à jour un tarif ou ajouter une ligne dans le comparatif.
C’est exactement le cas d’usage pour lequel les options pages ACF ont été conçues.
Le diagnostic Query Monitor : 331 requêtes options en une page
En activant Query Monitor sur cette page et en filtrant l’onglet Queries sur wp_options, le constat est immédiat :
SELECT option_value FROM wp_options WHERE option_name = 'options_pricing_catalog_plans_0_name' LIMIT 1
SELECT option_value FROM wp_options WHERE option_name = 'options_pricing_catalog_plans_0_price' LIMIT 1
SELECT option_value FROM wp_options WHERE option_name = 'options_pricing_catalog_plans_0_features_0_label' LIMIT 1
...

331 requêtes de ce type, toutes issues d’ACF Pro lisant les champs de l’options page un par un.
La cause est mécanique : ACF stocke les repeaters et les groupes imbriqués sous forme d’entrées atomiques dans wp_options. Un repeater avec 3 plans, chacun contenant 8 sous-champs, génère 24 entrées minimum. Ajoutez un tableau comparatif avec 15 lignes et 5 colonnes, et vous dépassez facilement les 80 entrées rien que pour cette section.
Chaque appel front à get_field() sur une valeur non présente dans le cache objet déclenche une requête SELECT unitaire. Multipliez par le nombre de champs lus dans les templates, et le compteur Query Monitor s’emballe.
Ce comportement est documenté et attendu par ACF. Le problème n’est pas dans ACF en lui-même, mais dans l’usage : lire directement depuis les options ACF sur le front d’une page stratégique et dense.
Ce qui ne suffit pas : optimiser les get_field()
Le premier réflexe est souvent de regrouper les appels :
<?php
// Plutôt que lire champ par champ
$name = get_field('plan_name', 'option');
$price = get_field('plan_price', 'option');
// Lire tout le groupe en une fois
$plan = get_field('pricing_catalog', 'option');
Ce pattern réduit le code, mais n’élimine pas les requêtes SQL : ACF résout quand même chaque sous-clé individuellement au moment de la lecture, si le cache objet n’est pas actif ou a été vidé.
Sur quelques champs simples, c’est acceptable. Sur une options page avec des repeaters imbriqués, le volume reste problématique.
La solution : un snapshot front agrégé
L’approche retenue préserve le confort éditorial ACF tout en changeant radicalement la stratégie de lecture front.
Principe
- L’admin continue d’éditer dans ACF — rien ne change côté back-office
- À chaque sauvegarde, un hook ACF normalise et sérialise tout le catalogue en une seule option WordPress dédiée
- Le front ne lit plus les clés
options_*— il charge uniquement ce snapshot
<?php
// Hook déclenché à chaque sauvegarde ACF
add_action('acf/save_post', function ($post_id) {
if ($post_id !== 'options') {
return;
}
$snapshot = [
'plans' => get_field('pricing_catalog_plans', 'option'),
'comparison' => get_field('pricing_comparison_rows', 'option'),
'settings' => get_field('pricing_settings', 'option'),
'enterprise' => get_field('pricing_enterprise_block', 'option'),
];
update_option('rl_pricing_front_snapshot_v1', $snapshot, true);
}, 20);
<?php
// Lecture front : une seule requête wp_options
function get_pricing_snapshot(): array {
static $snapshot = null;
if ($snapshot === null) {
$snapshot = get_option('rl_pricing_front_snapshot_v1', []);
}
return $snapshot;
}
L’option est stockée avec autoload = true (troisième paramètre de update_option), ce qui signifie que WordPress la charge dans son autoload initial avec toutes les autres options — sans aucune requête SQL supplémentaire au moment de la lecture front.
Ce que ça change dans Query Monitor
Avant optimisation :
- 393 requêtes SQL au total
- 331 requêtes sur
wp_options - Trace d’appel : 100 % ACF Pro via
get_field()
Après optimisation :
- 85 requêtes SQL au total
- 35 requêtes sur
wp_options - La page Tarifs ne génère plus aucune requête unitaire sur les champs ACF
La réduction est de -79 % sur les requêtes options et de -78 % sur le total. La latence perçue sur server-side rendering s’améliorait proportionnellement, surtout visible sur un environnement avec un serveur MySQL distant.
Checklist : identifier et corriger ce pattern
| Signal dans Query Monitor | Cause probable | Correction |
|---|---|---|
> 50 requêtes wp_options par page | Options page ACF avec repeaters | Snapshot sérialisé + autoload |
Requêtes options_*_0_*_1_* | Champs imbriqués lus à la volée | Agréger dans un snapshot à la sauvegarde |
| Mêmes clés options répétées | Template partiel appelé en boucle | Cache statique local dans le helper |
| Latence totale >> query time | Base distante (VPS, WP Engine…) | Redis Object Cache pour éliminer le SQL |
| Volume croissant avec le contenu | Architecture couplée ACF ↔ front | Découpler lecture et édition |
Points clés à retenir
L’idée centrale de ce use case est simple, mais elle mérite d’être formulée clairement :
- ACF est un excellent outil éditorial. Il n’est pas en cause.
- Le vrai problème : utiliser des repeaters imbriqués sur une options page, puis lire ces champs directement sur le front d’une page dense.
- La bonne séparation des responsabilités : ACF pour l’édition, une option normalisée pour la lecture.
- Ce pattern est simple, robuste et maintenable — le snapshot est un tableau PHP sérialisé, lisible, verstonable, et rechargé à chaque sauvegarde admin.
Ce type de décision architecturale fait partie du travail courant que je propose dans mon activité de développeur indépendant : identifier les vrais goulots, pas seulement les optimiser en surface.
Aller plus loin : Redis et la suite
Le snapshot décrit ici élimine les requêtes SQL unitaires sur wp_options, mais repose toujours sur MySQL pour la requête autoload initiale. Sur des sites à fort trafic ou des architectures à plusieurs serveurs web, l’étape suivante naturelle est l’ajout d’un cache objet persistant avec Redis.
Avec Redis Object Cache, le snapshot est servi depuis la mémoire Redis dès la deuxième requête HTTP. WordPress ne touche plus MySQL du tout pour cette donnée. Query Monitor l’indiquera clairement : la requête disparaît du log, remplacée par un hit cache.
À venir : un article dédié à la mise en place de Redis sur WordPress — configuration, invalidation du cache, cas limites avec les options autoloadées, et mesure de l’impact réel avec Query Monitor.
Que se passe-t-il si l'admin oublie de sauvegarder la page options après une modification ?
Le snapshot ne se met à jour qu’au acf/save_post. En pratique, l’admin sauvegarde systématiquement pour valider ses modifications — c’est le comportement normal. En cas de doute, on peut ajouter un mécanisme de régénération manuelle via WP-CLI (wp eval "rl_rebuild_pricing_snapshot();") ou un bouton admin dédié.
Ce pattern est-il applicable à d'autres types de contenu ACF ?
Oui. Le même principe s’applique à tout contenu dense lu en lecture seule sur le front : menus de navigation enrichis, configurations de blocs Gutenberg globaux, listes de ressources administrées. Le critère de déclenchement est simple : dès que Query Monitor montre plus de 30-40 requêtes issues d’une options page, un snapshot est généralement justifié.
Le snapshot pose-t-il des problèmes de mise en cache de page (Varnish, WP Rocket) ?
Non, c’est l’inverse. Un snapshot accélère la génération de la page côté PHP avant même que le cache de page n’entre en jeu. Sur un cache de page full-page, la réduction des requêtes SQL améliore le TTFB mesuré sur les requêtes non servies depuis le cache (robots, première visite, cache expiré).
Conversation
0 commentaires
Une question, un retour d’expérience ou une nuance utile ? Ajoute ton point de vue.
Pas encore de commentaires. Lance la discussion.