Le site est celui d’une fédération sportive régionale, dont la page d’accueil affiche un shortcode [derniers_resultats] introduit en 2014 par un prestataire depuis longtemps disparu. Ce shortcode liste les cinq derniers résultats de compétition, avec le nom du club, le score et une mini-image. Personne dans l’équipe actuelle n’avait jamais lu son code avant qu’un audit de performance, déclenché par une plainte sur la lenteur de la page d’accueil, ne s’y attarde.
La page d’accueil reçoit l’essentiel du trafic du site, en particulier le lundi matin après un week-end de compétitions, moment où elle est justement la plus lente.
Ce que l’audit a trouvé
L’onglet requêtes de Query Monitor a révélé que le shortcode exécutait, à chaque affichage de page, cinq requêtes SQL séparées pour reconstituer une seule liste de cinq résultats : une requête pour récupérer les cinq derniers identifiants de résultats via get_posts(), puis, dans une boucle, quatre requêtes supplémentaires par résultat pour aller chercher le nom du club, le score, l’image et le nom de la compétition associée, chacune stockée dans une table personnalisée séparée créée par le prestataire d’origine plutôt que dans les métadonnées WordPress standard.
Sur cinq résultats affichés, cela représentait donc jusqu’à vingt et une requêtes au total pour un seul shortcode, exécutées à chaque chargement de la page d’accueil, cache de page mis à part.
Le code d’origine, tel qu’il a été retrouvé
function afficher_derniers_resultats() {
global $wpdb;
$ids = $wpdb->get_col(
"SELECT id_resultat FROM {$wpdb->prefix}fede_resultats ORDER BY date_resultat DESC LIMIT 5"
);
$html = '<ul>';
foreach ( $ids as $id ) {
$club = $wpdb->get_var( $wpdb->prepare(
"SELECT nom_club FROM {$wpdb->prefix}fede_clubs WHERE id_resultat = %d", $id
) );
$score = $wpdb->get_var( $wpdb->prepare(
"SELECT score FROM {$wpdb->prefix}fede_resultats WHERE id_resultat = %d", $id
) );
$image = $wpdb->get_var( $wpdb->prepare(
"SELECT url_image FROM {$wpdb->prefix}fede_images WHERE id_resultat = %d", $id
) );
$competition = $wpdb->get_var( $wpdb->prepare(
"SELECT nom_competition FROM {$wpdb->prefix}fede_competitions WHERE id_resultat = %d", $id
) );
$html .= "<li>{$club} — {$score} ({$competition})</li>";
}
$html .= '</ul>';
return $html;
}
add_shortcode( 'derniers_resultats', 'afficher_derniers_resultats' );
Pourquoi c’est un problème même avec un cache de page
Le site utilisait déjà un cache de page FastCGI pour les visiteurs anonymes, ce qui masquait en partie le problème pour la majorité du trafic. Mais deux populations restaient exposées à la lenteur brute : les administrateurs connectés qui consultent la page d’accueil pour vérifier les résultats juste après une compétition, moment où le cache est justement invalidé le plus souvent, et les robots d’indexation qui, selon la configuration, peuvent ne pas bénéficier du même cache que les visiteurs humains.

La réécriture avec une seule requête préparée
La réécriture a consisté à remplacer les cinq requêtes par une seule, avec des jointures SQL, en s’appuyant sur $wpdb->prepare() pour garder une requête sécurisée contre les injections.
function afficher_derniers_resultats() {
global $wpdb;
$cached = get_transient( 'fede_derniers_resultats' );
if ( false !== $cached ) {
return $cached;
}
$rows = $wpdb->get_results(
"SELECT r.score, c.nom_club, i.url_image, cp.nom_competition
FROM {$wpdb->prefix}fede_resultats r
LEFT JOIN {$wpdb->prefix}fede_clubs c ON c.id_resultat = r.id_resultat
LEFT JOIN {$wpdb->prefix}fede_images i ON i.id_resultat = r.id_resultat
LEFT JOIN {$wpdb->prefix}fede_competitions cp ON cp.id_resultat = r.id_resultat
ORDER BY r.date_resultat DESC
LIMIT 5"
);
$html = '<ul>';
foreach ( $rows as $row ) {
$html .= sprintf(
'<li>%s — %s (%s)</li>',
esc_html( $row->nom_club ),
esc_html( $row->score ),
esc_html( $row->nom_competition )
);
}
$html .= '</ul>';
set_transient( 'fede_derniers_resultats', $html, 15 * MINUTE_IN_SECONDS );
return $html;
}
add_shortcode( 'derniers_resultats', 'afficher_derniers_resultats' );
Invalider le transient au bon moment
Un transient figé quinze minutes ne suffisait pas seul : il fallait aussi l’invalider immédiatement quand un nouveau résultat est saisi, pour que les administrateurs voient le changement sans attendre. Un hook déclenché à l’enregistrement d’un résultat s’en charge.
add_action( 'fede_resultat_enregistre', function () {
delete_transient( 'fede_derniers_resultats' );
} );
Ce que la réécriture n’a pas traité
Cette intervention n’a pas cherché à migrer les tables personnalisées fede_clubs, fede_images et fede_competitions vers des blocs Gutenberg ou une structure de métadonnées WordPress standard : le volume de code dépendant de ce schéma ailleurs sur le site rendait cette migration disproportionnée par rapport au gain attendu, et elle a été mise de côté comme un chantier séparé, plus risqué.
- Une requête avec jointures remplace cinq requêtes séparées dans une boucle.
- Un transient de courte durée absorbe les répétitions entre deux invalidations.
- Le hook d’invalidation garde la fraîcheur des données pour les cas où elle compte.
Un shortcode qu’on n’a jamais eu besoin d’ouvrir reste, la plupart du temps, un shortcode qu’on n’a jamais eu besoin d’auditer — jusqu’au jour où la page qui l’utilise devient la plus visitée du site.
En résumé
Le remplacement des vingt et une requêtes potentielles par une seule requête avec jointures, doublée d’un cache transitoire de quinze minutes, a fait passer le temps de génération du bloc de résultats de 340 à 12 millisecondes sur la page d’accueil. L’essentiel du travail n’était pas d’écrire du code plus élégant, mais de comprendre d’abord ce que le code existant faisait vraiment, requête par requête.