# Deux profileurs PHP face au même goulet : ce qu’ils ne montrent pas

> Une même requête WordPress lente, passée dans deux profileurs différents. Chacun éclaire une partie du problème et laisse l'autre dans l'ombre.

- Auteur : Clément Hadrot
- Publié le : 2025-04-28
- Mis à jour le : 2025-04-28
- Catégorie : Performance
- URL : https://wpmoderne.dev.wordpress-developpement.fr/performance/deux-profileurs-php-meme-goulet-comparatif/

## L’essentiel

- Un profileur montre l'appel de fonction, l'autre le temps réel écoulé
- Aucun des deux ne voit ce qui se passe hors du processus PHP
- Croiser les deux a été nécessaire pour trouver la cause

« Pourquoi cette page met-elle 1,2 seconde à s'afficher ? » La question, posée par un client dont le catalogue de formations professionnelles venait de doubler de taille, appelait une réponse précise. Deux outils de profilage ont été mobilisés sur la même page, à quelques minutes d'écart, dans l'espoir qu'ils raconteraient la même histoire. Ils l'ont racontée différemment, chacun révélant une face du problème que l'autre laissait dans l'angle mort.

Un profileur PHP observe l'exécution du code au moment où elle se produit, en enregistrant le temps passé dans chaque fonction appelée. Deux familles d'outils coexistent : ceux qui s'appuient sur des extensions PHP bas niveau, capables de mesurer le temps CPU réel de chaque fonction, et ceux qui s'appuient sur l'instrumentation applicative de WordPress, plus légers mais limités à ce que le cœur et les extensions exposent volontairement.

## Ce que Query Monitor a montré en premier

Installé directement sur l'environnement de recette, `Query Monitor` a rapidement pointé du doigt une requête `WP_Query` particulièrement lente, exécutée pour afficher les formations liées à une thématique donnée via une taxonomie personnalisée :

```
$query = new WP_Query( array(
    'post_type'      => 'formation',
    'tax_query'      => array( array(
        'taxonomy' => 'thematique',
        'field'    => 'slug',
        'terms'    => $slug,
    ) ),
    'posts_per_page' => 20,
    'orderby'        => 'meta_value_num',
    'meta_key'       => 'date_session',
) );
```

L'onglet des requêtes indiquait un temps de 380 millisecondes pour cette seule requête, contre quelques millisecondes pour les autres. La cause semblait limpide : un tri par `meta_value_num` combiné à une jointure de taxonomie, sans index approprié sur la table `wp_postmeta`.

## Ce qu'un profileur bas niveau a révélé ensuite

> L'essentiel à retenir : Un profileur montre l'appel de fonction, l'autre le temps réel écoulé ; Aucun des deux ne voit ce qui se passe hors du processus PHP ; Croiser les deux a été nécessaire pour trouver la cause

Un second passage, cette fois avec un profileur d'exécution PHP capable de produire un graphe d'appel complet de la requête, a montré une réalité plus nuancée. Le temps passé réellement dans MySQL pour cette requête ne représentait que 210 des 380 millisecondes signalées par Query Monitor. Le reste, environ 170 millisecondes, était consommé par le traitement PHP en aval : la boucle d'affichage rappelait `get_field()` pour chaque formation, une fonction d'un champ personnalisé qui déclenchait elle-même une lecture supplémentaire, invisible dans l'onglet des requêtes SQL de Query Monitor puisqu'elle passait par le cache d'objets et non par une nouvelle requête.

Ce que Query Monitor n'a jamais montré, faute d'instrumentation à ce niveau : le temps passé dans le sérialisation et la désérialisation des valeurs de champs complexes stockées en JSON dans les métadonnées, une opération purement PHP, sans aucune requête SQL associée, mais qui pesait presque autant que la requête elle-même.

## Ce qu'aucun des deux outils ne voyait

Ni l'un ni l'autre profileur n'a mesuré le temps réseau entre le serveur applicatif et le serveur de base de données, hébergés sur deux machines distinctes chez cet hébergeur. Un test manuel avec l'utilitaire `mysqlslap`, exécuté directement depuis le serveur web, a révélé une latence réseau de 15 millisecondes par requête, négligeable individuellement mais qui s'additionnait sur les requêtes secondaires déclenchées par les champs personnalisés.

## Ce que la comparaison a permis de conclure

| Aspect mesuré | Query Monitor | Profileur bas niveau |
| --- | --- | --- |
| Temps SQL par requête | Oui, agrégé | Oui, détaillé par appel |
| Temps PHP pur (hors SQL) | Non détaillé | Oui, fonction par fonction |
| Latence réseau vers la base | Non mesurée | Non mesurée |
| Facilité d'installation en production | Élevée | Plus contraignante |

## La correction retenue

- Un index composite a été ajouté sur la table de métadonnées pour accélérer le tri par date de session.
- Les valeurs de champs personnalisés ont été mises en cache dans un transient à courte durée, évitant leur recalcul à chaque affichage.
- Le temps de génération total est redescendu à 340 millisecondes, une amélioration que ni l'un ni l'autre outil, utilisé seul, n'aurait permis de chiffrer aussi précisément.

> Un profileur raconte ce qu'il sait mesurer, pas ce qui se passe réellement : croiser deux instruments différents révèle souvent plus qu'en pousser un seul jusqu'à ses limites.

## En résumé

Query Monitor a désigné la requête SQL coupable ; le profileur bas niveau a révélé qu'elle n'expliquait qu'une partie du temps perdu. Aucun des deux, seul, n'aurait donné le tableau complet. Diagnostiquer un goulet d'étranglement réel demande souvent de superposer plusieurs outils, chacun aveugle à ce que l'autre observe.
