'fields' => 'ids' : trois mots glissés dans un tableau WP_Query, et l’endpoint REST d’un réseau d’artisans qui répertorie plusieurs centaines de fiches est passé d’un temps de réponse pénible à quelque chose de largement acceptable pour le front headless qui le consomme.
Le problème initial était classique : une route personnalisée bouclait sur une WP_Query qui chargeait l’objet complet de chaque article, avec tout son contenu, ses métadonnées et ses taxonomies, alors que l’écran de listing du front n’avait besoin que d’un identifiant pour construire ses liens et déclencher un chargement différé du détail.
Le problème caché derrière une boucle innocente
La callback initiale ressemblait à ceci, et fonctionnait très bien avec une dizaine d’artisans en base :
function artisans_get_liste( $request ) {
$query = new WP_Query( array(
'post_type' => 'artisan',
'posts_per_page' => 200,
) );
$result = array();
foreach ( $query->posts as $post ) {
$result[] = array(
'id' => $post->ID,
'titre' => $post->post_title,
);
}
return rest_ensure_response( $result );
}
Le souci n’est pas visible dans le code, mais dans ce que WordPress charge en coulisses : chaque objet WP_Post complet, avec son contenu intégral, même si la callback n’en utilise ensuite qu’une infime partie.
La correction : restreindre les colonnes chargées
L’argument fields de WP_Query accepte la valeur ids, qui limite la requête SQL sous-jacente à la seule colonne des identifiants, sans jamais hydrater les objets WP_Post complets.

function artisans_get_liste( $request ) {
$query = new WP_Query( array(
'post_type' => 'artisan',
'posts_per_page' => 200,
'fields' => 'ids',
) );
$result = array();
foreach ( $query->posts as $id ) {
$result[] = array(
'id' => $id,
'titre' => get_the_title( $id ),
);
}
return rest_ensure_response( $result );
}
Ce n’est pas gratuit : get_the_title() déclenche un appel supplémentaire par identifiant, mais celui-ci passe par le cache d’objets WordPress, ce qui reste largement moins coûteux que l’hydratation complète de deux cents objets WP_Post lors de la requête initiale.
Deux variantes selon le besoin réel du front
'fields' => 'ids': uniquement les identifiants, à combiner avec des appels ciblés ensuite'fields' => 'id=>parent': identifiants et parents, utile pour reconstruire une hiérarchie sans requête supplémentaire
Pour un catalogue d’artisans organisé par métier, la seconde variante a permis de reconstruire côté front une arborescence par catégorie de métier sans requête additionnelle, simplement en croisant les identifiants et leurs parents renvoyés par la première requête.
Mesurer l’impact avant de généraliser la recette
Avant de propager cette approche à d’autres routes du même projet, un relevé simple a comparé le temps de réponse moyen de l’endpoint avant et après la modification, sur un échantillon de cinquante appels consécutifs, un jour de trafic ordinaire.
| Mesure | Avant (objets complets) | Après (fields => ids) |
|---|---|---|
| Temps de réponse moyen | Nettement plus lent | Sensiblement réduit |
| Requêtes SQL déclenchées | Une par artisan hydraté | Une seule requête groupée |
Ce relevé, volontairement simple et réalisé sans outil de profilage sophistiqué, a suffi à convaincre l’équipe de reproduire la même correction sur deux autres routes du projet qui souffraient du même défaut de conception initial.
Ce que le front fait ensuite de cette liste
Le front headless reçoit désormais une liste légère d’identifiants, qu’il utilise pour construire les liens de la page d’index. Le détail complet de chaque artisan n’est chargé que lorsque l’utilisateur clique réellement sur une fiche, via une seconde route qui, elle, hydrate l’objet complet parce que le besoin le justifie à ce moment précis.
Charger le détail complet d’une ressource pour n’en afficher qu’un identifiant, c’est payer le prix fort pour une information dont on n’a pas encore besoin.
En résumé
L’argument fields de WP_Query reste l’un des leviers les plus simples et les plus mal connus pour alléger un endpoint REST consommé par un front headless. Restreindre la requête aux seuls identifiants, puis hydrater à la demande via le cache d’objets, évite de payer une hydratation complète pour un listing qui n’en a pas besoin. Cette recette ne traite volontairement pas la question du cache HTTP, qui viendrait s’ajouter en complément une fois cette première optimisation en place.