vendredi 25 septembre 2026

À propos

Contact

Headless & API

Requêtes WPGraphQL lentes : complexité, profondeur et N+1

Une page qui met huit secondes à charger côté front alors que WPGraphQL tourne sur un petit site : diagnostic des trois causes les plus fréquentes.

Par Clément Hadrot • 9 mars 2023 • 5 min de lecture • Aucun commentaire
Requêtes WPGraphQL lentes : complexité, profondeur et N+1

Le symptôme est arrivé sans prévenir : une page produit qui affichait des articles liés en carrousel s’est mise à charger en six à huit secondes, alors que le même site tenait sous 400 millisecondes deux semaines plus tôt. Aucune modification côté front, aucune montée de version de WordPress. Seul changement : un champ personnalisé ajouté au schéma pour remonter les avis clients associés à chaque produit.

Ce genre de dégradation silencieuse est typique de WPGraphQL mal outillé : la requête reste syntaxiquement valide, elle renvoie toujours la bonne donnée, mais son coût d’exécution explose. Voici comment on isole le problème sans deviner.

Symptôme : une requête qui ralentit avec le contenu

Premier réflexe utile : vérifier si le temps de réponse est corrélé au volume de données. Sur ce projet, la requête list qui remonte dix produits avec leurs avis passait de 90 millisecondes à 6 secondes dès que chaque produit avait plus de trente avis en base. C’est la signature d’un problème qui grossit avec les données, pas d’une simple requête lourde ponctuelle.

Diagnostic : activer le tracing

WPGraphQL peut être exécuté en mode debug en définissant la constante GRAPHQL_DEBUG à true dans wp-config.php. Combiné à Query Monitor, cela affiche le nombre de requêtes SQL générées par l’exécution GraphQL et le détail des résolveurs appelés. C’est là qu’on a repéré le vrai coupable.

L'essentiel à retenir : Le tracing révèle où part le temps de résolution ; La profondeur de requête explique les régressions imprévisibles ; Le N+1 se cache dans les résolveurs personnalisés

Diagnostic : la profondeur qui explose la combinatoire

Le champ ajouté remontait les avis, et chaque avis remontait à son tour l’auteur, et chaque auteur remontait ses propres avis publiés ailleurs sur le site. Une requête front qui semblait raisonnable (trois niveaux d’imbrication) déclenchait en réalité une combinatoire à cinq ou six niveaux dès que le client incluait un fragment réutilisable un peu trop généreux.

Dans les réglages de WPGraphQL (menu GraphQL > Réglages), l’option de limite de profondeur de requête permet de fixer un plafond au nombre de niveaux imbriqués acceptés. Une requête qui dépasse ce plafond est rejetée avant même d’être exécutée, ce qui évite qu’un fragment mal maîtrisé côté front ne mette le serveur à genoux.

Fixer une profondeur raisonnable

  • Commencer par observer la profondeur réelle des requêtes utilisées en production via le tracing.
  • Fixer la limite un ou deux niveaux au-dessus de ce qui est réellement utilisé.
  • Désactiver l’introspection publique en parallèle, pour éviter qu’un client externe ne découvre et n’exploite des chemins profonds non prévus.

Diagnostic : le vrai coupable, un résolveur en N+1

Le champ personnalisé pour les avis avait été enregistré avec register_graphql_field et allait chercher les commentaires produit par produit, avec un get_comments() à chaque résolution de champ plutôt qu’une requête groupée. Pour dix produits, cela donnait dix requêtes rien que pour les avis, puis une requête supplémentaire par auteur d’avis pour ses informations de profil : le fameux problème N+1.

// Mauvais : une requête SQL par produit résolu
register_graphql_field( 'Product', 'topReviews', [
    'type'    => [ 'list_of' => 'Comment' ],
    'resolve' => function( $product ) {
        return get_comments( [ 'post_id' => $product->ID ] );
    },
] );

La correction consiste à s’appuyer sur le système de chargement différé de WPGraphQL, qui regroupe les identifiants demandés par la même exécution de requête et les résout en un seul appel, plutôt qu’un appel par nœud parent.

Correctif appliqué

On a réécrit le résolveur pour empiler les identifiants de produits dans le contexte de la requête, puis exécuter un seul WP_Comment_Query avec un tableau de post__in une fois tous les produits parents résolus. Résultat : la requête qui déclenchait 340 requêtes SQL dans le pire cas observé est redescendue à moins de dix, quel que soit le nombre de produits demandés dans la même page.

Un champ personnalisé qui « marche » en local sur trois produits de test ne dit rien de son comportement sur un catalogue de mille références. Le test de charge sur un jeu de données réaliste devrait faire partie de la revue de code, pas être découvert en production.

Prévention pour la suite

Trois réflexes à garder pour chaque nouveau champ ajouté au schéma : activer le tracing avant de merger, fixer une limite de profondeur cohérente avec l’usage réel du front, et se méfier systématiquement de tout résolveur qui appelle une fonction de requête WordPress à l’intérieur d’une boucle sur les nœuds parents. Le cache, lui, n’aurait rien réglé ici : il aurait simplement caché le problème un peu plus longtemps.

Partager :

À propos de l'auteur

Clément Hadrot

Développeur WordPress, passionné par Elementor, le FSE et l’automatisation par IA.

Voir tous ses articles

Dans la même veine

À lire aussi