optimisation core web vitals 8 min

$wpdb : la classe WordPress qui fait gagner 300ms sur votre TTFB

Découvrez comment la classe wpdb peut réduire le TTFB de votre site WordPress. Requêtes préparées, cache interne, remplacement de WP_Query : guide complet.

Par Julien Morel
Partager

Mardi 14h, un client nous envoie un rapport GTmetrix pour sa boutique WooCommerce : TTFB à 2,8 secondes. Dans le fichier functions.php de son thème enfant, on a trouvé une boucle de get_posts() imbriquée dans une autre boucle, sans aucune mise en cache, qui déclenchait 147 requêtes SQL sur chaque page catégorie. Le tout pour afficher une liste de produits apparentés. En migrant les appels vers $wpdb avec des requêtes ciblées et un cache maison minimal, on a ramené ce TTFB à 1,2 seconde. Ce n’est pas un cas isolé. La classe wpdb reste le parent pauvre de la performance WordPress, souvent réduite à un vieux réflexe de dev « sécurisé ». Pourtant, c’est le levier le plus direct pour tailler dans le temps de réponse serveur. On va voir ce que cette classe a vraiment dans le ventre, et comment l’utiliser sans transformer son thème en usine à gaz.

WP_Query n’est pas toujours la solution, et voici ce qu’elle vous coûte

Tous les développeurs WordPress connaissent WP_Query, et la plupart l’utilisent pour tout : requêtes d’articles, listes filtrées, agrégations simples. Le problème, c’est que chaque instance de WP_Query instancie toute la machine interne (parsing de taxonomies, post meta, hooks). Sur une page d’archive, c’est transparent. Sur une page d’accueil avec six blocs visuels différents, c’est une catastrophe.

Quand tu écris ceci :

$recent = new WP_Query(['posts_per_page' => 5]);

WordPress exécute en réalité deux à trois requêtes SQL, plus une phase d’hydratation des objets WP_Post. Si cette ligne est appelée dans une boucle de template pour chaque catégorie d’une grille, le nombre total de requêtes explose. Avec $wpdb, tu peux économiser jusqu’à 60 % du temps CPU côté serveur sur ces affichages composés, simplement parce que tu ne fais qu’une seule requête maigre :

global $wpdb;
$ids = $wpdb->get_col("SELECT ID FROM {$wpdb->posts} WHERE post_type = 'post' AND post_status = 'publish' ORDER BY post_date DESC LIMIT 5");

Ensuite tu passes ces IDs à un constructeur d’objets, si tu as vraiment besoin d’objets WP_Post. Mais bien souvent, pour un simple affichage de titres et de dates, tu n’as besoin que des champs bruts. Cette approche est comparable au choix d’un store Zustand pour gérer un état complexe plutôt qu’un Redux générique : tu ne charges que le strict nécessaire, et le state management React avec Zustand t’a probablement déjà enseigné cette leçon sur le front. La même logique vaut pour les données en base.

Les trois méthodes wpdb qui changent la face d’une page archive

Le Codex insiste sur get_results(), mais c’est la méthode la plus lourde. Pour la plupart des besoins de la vie d’un template, get_row() et get_var() suffisent.

Prenons un bloc « dernier commentaire » sur une sidebar. Avec get_results(), tu récupères un tableau d’objets qu’il faut parcourir. Avec get_row(), tu obtiens directement l’unique ligne dont tu as besoin :

$last_comment = $wpdb->get_row("SELECT comment_content, comment_date FROM {$wpdb->comments} WHERE comment_approved = '1' ORDER BY comment_date DESC LIMIT 1");

Pas de boucle, pas de foreach. Moins de code, moins d’allocations.

get_var() est encore plus radical. Si tu dois simplement vérifier une condition : « est-ce que l’auteur a déjà publié aujourd’hui ? », une requête SELECT COUNT(*) couplée à get_var() renvoie un scalaire immédiatement réutilisable. Plus besoin de charger une collection pour un test booléen.

L’astuce vraiment sous-exploitée, c’est l’offset de colonne sur get_var(). Imaginons que tu veuilles la date du dernier article d’une catégorie, pour l’afficher au survol d’un lien. Au lieu d’un get_row() puis d’accéder à la propriété, tu peux viser directement la colonne :

$last_date = $wpdb->get_var("SELECT post_title, post_date FROM {$wpdb->posts} WHERE post_type = 'post' AND post_status = 'publish' ORDER BY post_date DESC LIMIT 1", 1);

Le deuxième argument, 1, cible la colonne post_date (index 1). Ça a l’air d’un détail, mais cumulé sur une centaine de widgets, le gain d’allocation objet n’est pas négligeable.

Requêtes préparées : le non-respect de cette règle plombe votre sécurité et vos performances

Tu le sais déjà : utiliser une variable GET directement dans une requête SQL, c’est ouvrir la porte aux injections. La fonction prepare() est obligatoire. Pourtant, elle est souvent vue comme une contrainte de sécurité, rarement comme un outil de performance.

Le serveur de base de données (MySQL, MariaDB) met en cache les plans d’exécution des requêtes préparées. Si la requête change à chaque appel parce que la valeur est concaténée dedans, le plan n’est jamais réutilisé. Avec prepare(), la structure reste identique, seule la valeur du paramètre change. Résultat : le SGBD réutilise le plan d’exécution, ce qui économise quelques millisecondes à chaque appel. Sur du trafic, ça se voit.

Mauvaise pratique qui traîne encore :

$id = $_GET['article'];
$article = $wpdb->get_row("SELECT * FROM {$wpdb->posts} WHERE ID = $id");

Correct :

$id = intval($_GET['article']);
$article = $wpdb->get_row($wpdb->prepare("SELECT post_title, post_date FROM {$wpdb->posts} WHERE ID = %d", $id));

L’autre piège, c’est LIKE. Le placeholder %s échappe déjà les chaînes, mais il faut doubler le % à l’intérieur de la requête : LIKE '%%%s%%'. Sinon WordPress interprète le pourcentage comme un placeholder et casse la requête. On a perdu une journée là-dessus sur un moteur de recherche interne.

Le cache interne de $wpdb : un allié à activer sans modération

Peu de développeurs le savent : $wpdb met en cache les résultats des requêtes au sein d’une même requête HTTP. Si tu appelles deux fois la même requête SQL dans le même cycle de chargement de page, la base de données n’est sollicitée qu’une fois.

Ce comportement est actif par défaut, mais il a une condition : il faut utiliser exactement la même chaîne SQL. Deux requêtes identiques sémantiquement mais avec des espaces différents, ou des alias différents, seront traitées comme distinctes. Centraliser ses appels SQL dans une fonction helper et normaliser les chaînes élimine ce risque.

Auditer ses requêtes SQL sans plugin ni extension

Tu veux savoir combien de requêtes ton thème exécute, et lesquelles sont lentes ? Pas besoin d’installer Query Monitor en production. WordPress possède une constante magique qui change la vie.

Ajoute ceci dans ton wp-config.php, en développement local :

define('SAVEQUERIES', true);

Ensuite, dans le pied de page de ton thème (pour du debug temporaire), insère :

if (current_user_can('administrator')) {
    global $wpdb;
    echo '<pre>';
    print_r($wpdb->queries);
    echo '</pre>';
}

Tu obtiendras la liste complète des requêtes SQL exécutées sur la page, avec pour chacune : le temps d’exécution en secondes, la stack d’appels. La première chose qu’on voit en général : des doublons évités par le cache, et des requêtes de meta qui partent en JOIN cartésien. L’analyse de ces logs, c’est le genre de travail qu’on peut faire avec n’importe quel éditeur moderne. Peu importe que ton environnement tourne avec Claude Code ou Cursor, l’important est d’avoir des données brutes sous les yeux. Le débat Claude Code vs Cursor IDE existe précisément parce que les outils d’analyse évoluent vite ; la logique d’audit, elle, ne bouge pas.

Une fois les requêtes identifiées, tu peux ajouter des index manquants via un plugin comme Index WP MySQL ou directement en base. Mais avant d’indexer, remplace ce qui peut l’être par des appels $wpdb plus légers. L’optimisation des requêtes contribue directement à abaisser le TTFB, et c’est un levier bien plus efficace que de compresser le HTML. Si tu veux comprendre comment ce temps de réponse s’intègre dans les signaux Core Web Vitals que Google suit, le guide sur l’optimisation Core Web Vitals détaille la place exacte du serveur dans l’équation.

wpdb et l’API REST : comment le contexte a changé après Gutenberg

Avec l’avènement de l’éditeur de blocs, beaucoup de données transitent désormais par l’API REST de WordPress. Les endpoints natifs utilisent massivement WP_Query et les fonctions abstraites. Mais si vous développez un endpoint personnalisé pour une app headless, $wpdb redevient pertinent.

Prenons un endpoint qui doit renvoyer un classement agrégeant des données de plusieurs tables custom. Passer par WP_Query pour chaque type de contenu ajouterait des couches d’abstraction qui nuisent au temps de réponse de l’API. Une requête SQL bien écrite, encapsulée dans un callback register_rest_route(), avec son propre cache transitoire, livre les données en une fraction de seconde.

Le piège moderne, c’est l’hybridation : un site qui mélange des blocs chargés côté serveur (rendu PHP classique) et des blocs qui fetch des données via l’API en front. Dans ce second cas, le TTFB ne reflète plus seulement le temps de génération PHP, mais la somme des requêtes API que le navigateur doit résoudre avant de rendre le contenu. Si ces endpoints reposent sur des empilements de WP_Query, la dégradation est double : le serveur souffre, et le LCP client aussi. Ramener la couche data à des appels $wpdb propres, c’est raccourcir la chaîne des dépendances.

Questions fréquentes

Doit-on vraiment éviter WP_Query dans un thème ?

WP_Query reste l’outil standard pour la boucle principale et les scénarios où les filtres et les hooks sont nécessaires. Le problème, c’est la surcharge : une dizaine d’instances sur une même page, c’est trop. Pour des listes secondaires, préferer $wpdb avec un cache manuel de 30 secondes diminue le bruit SQL sans perdre en maintenabilité.

Quid de la compatibilité avec les plugins de cache ?

Les plugins de cache d’objet (Redis, Memcached) interceptent les appels WP_Query mais pas nécessairement les appels $wpdb bruts. Si tu utilises un cache persistant, vérifie que tes requêtes $wpdb ne contournent pas les mécanismes d’invalidation. La bonne pratique consiste à encapsuler la requête dans wp_cache_get() / wp_cache_set() pour bénéficier du cache d’objet, quel que soit le mode d’interrogation de la base.

Peut-on utiliser $wpdb pour des requêtes sur des tables externes ?

Oui, en instanciant un nouvel objet wpdb avec les identifiants de connexion de la base externe. Cela permet de lire ou d’écrire dans une autre base sans interférer avec la connexion principale de WordPress. Attention à bien fermer la connexion après usage si le volume est important, pour ne pas saturer le pool.

Articles similaires

Julien Morel

Julien Morel

Ancien dev front React passé SEO technique après une migration e-commerce qui a fait perdre 60% du trafic organique à son employeur en une nuit (fichier robots.txt oublié en staging). Depuis, il écrit pour que ça n'arrive à personne d'autre et teste sur ses propres side-projects avant de publier quoi que ce soit.

Cet article est publie a titre informatif. Faites vos propres recherches avant toute decision.