Optimiser les performances PHP
La majorité des lenteurs d’une application PHP viennent de quelques causes récurrentes : OPcache mal configuré, requêtes N+1 invisibles derrière un ORM, ou optimisation menée sans mesure préalable. Profiler avant d’optimiser évite de perdre du temps sur un problème qui n’existe pas.
Sommaire
Le problème #
Une application PHP qui répond lentement pousse souvent à une réaction instinctive : réécrire une boucle, ajouter du cache un peu partout, changer de framework. Ce genre d’optimisation “au feeling” corrige parfois un vrai problème par hasard, mais tout aussi souvent, elle rend le code plus complexe sans améliorer réellement le temps de réponse — parce que le vrai goulot d’étranglement se trouvait ailleurs.
Les lenteurs PHP ont des causes récurrentes et bien identifiées : un moteur qui recompile le code à chaque requête, un ORM qui exécute silencieusement des centaines de petites requêtes SQL au lieu d’une seule, ou tout simplement l’absence de toute mesure avant de se lancer dans une optimisation.
L’idée générale #
Trois leviers couvrent la grande majorité des gains de performance PHP en usage courant.
OPcache. PHP est un langage interprété : sans cache, chaque requête HTTP oblige le moteur à relire le fichier source, l’analyser (parsing) et le compiler en bytecode avant de l’exécuter — un travail répété inutilement si le fichier n’a pas changé depuis la requête précédente. OPcache conserve ce bytecode compilé en mémoire partagée, pour toutes les requêtes suivantes, jusqu’à ce que le fichier source soit modifié. Sur une application avec un framework et de nombreux fichiers, l’effet est souvent immédiat et significatif, pour un coût de configuration minimal.
Le problème N+1. Quand on affiche une liste d’éléments (des articles, par exemple) et que, pour chacun, on charge séparément une donnée liée (l’auteur de chaque article), le code exécute une requête pour récupérer la liste, puis une requête supplémentaire par élément — soit N+1 requêtes pour N éléments. Avec 20 articles, ça fait 21 requêtes là où 2 suffiraient. Le problème est particulièrement insidieux avec un ORM, dont le lazy loading déclenche des requêtes de façon transparente, sans que le code source laisse deviner combien de requêtes SQL sont réellement exécutées.
Profiler avant d’optimiser. Un profiler (Xdebug, Blackfire, ou même un simple chronométrage manuel autour de sections suspectes) mesure objectivement où le temps est réellement passé : dans une boucle CPU-intensive, dans des appels SQL répétés, dans un appel réseau externe lent. Sans cette mesure, l’optimisation devient une suite de paris — parfois gagnants, souvent une perte de temps sur un composant qui n’était de toute façon pas le facteur limitant.
Analogie du quotidien #
Optimiser sans profiler, c’est comme diagnostiquer une panne de voiture en changeant des pièces au hasard parce que “ça pourrait être ça” — les bougies, puis la batterie, puis le filtre à air — sans jamais brancher la valise de diagnostic électronique. On finit parfois par tomber sur la bonne pièce, après beaucoup de temps et d’argent dépensés inutilement sur des pièces qui fonctionnaient très bien.
Le profiler, c’est la valise de diagnostic : elle indique précisément quel capteur remonte une valeur anormale, avant même d’ouvrir le capot. OPcache et le problème N+1, dans cette analogie, sont deux pannes extrêmement courantes et bien documentées — l’équivalent d’une bougie encrassée ou d’une durite mal serrée — qu’un bon garagiste vérifie en priorité avant de chercher plus loin.
Diagramme #
Exemple de code #
Code vulnérable #
<?php
// Problème N+1 : une requête pour les articles, puis une par article pour l'auteur
function afficherArticlesAvecAuteur(PDO $pdo): array
{
$stmt = $pdo->query('SELECT id, titre, auteur_id FROM articles LIMIT 20');
$articles = $stmt->fetchAll();
foreach ($articles as &$article) {
// Exécutée 20 fois : une requête SQL par article affiché
$stmtAuteur = $pdo->prepare('SELECT nom FROM auteurs WHERE id = ?');
$stmtAuteur->execute([$article['auteur_id']]);
$article['auteur_nom'] = $stmtAuteur->fetchColumn();
}
return $articles; // 21 requêtes exécutées au total
}
; php.ini : OPcache désactivé ou mal configuré
opcache.enable=0
Code corrigé #
<?php
// Une seule requête, via une jointure, au lieu de 21
function afficherArticlesAvecAuteur(PDO $pdo): array
{
$sql = '
SELECT a.id, a.titre, u.nom AS auteur_nom
FROM articles a
JOIN auteurs u ON u.id = a.auteur_id
LIMIT 20
';
$stmt = $pdo->query($sql);
return $stmt->fetchAll(); // 1 requête au total
}
; php.ini : OPcache activé et dimensionné pour l'application
opcache.enable=1
opcache.memory_consumption=128
opcache.max_accelerated_files=10000
opcache.validate_timestamps=0 ; en production : désactive la vérification à chaque requête,
; le cache doit alors être vidé explicitement à chaque déploiement
Quand s’en préoccuper ? #
- Dès la mise en production : OPcache doit être activé et correctement dimensionné (assez de mémoire et de slots de fichiers pour couvrir tout le code de l’application) — c’est une des optimisations les moins coûteuses et les plus impactantes qui existent.
- Dès qu’une page affiche une liste d’éléments accompagnés d’une donnée liée (auteur, catégorie, statistiques) : c’est le terrain classique du problème N+1, particulièrement avec un ORM en lazy loading par défaut.
- Avant toute session d’optimisation, systématiquement : profiler d’abord, corriger ensuite, puis mesurer à nouveau pour confirmer le gain réel — sans cette boucle, on ne sait jamais si le changement a vraiment aidé.
- Lors d’une revue de code touchant une boucle qui interroge la base de données ou un service externe à chaque itération : c’est le signal le plus fiable pour repérer un N+1 avant qu’il n’atteigne la production.
Points importants #
opcache.validate_timestamps=0accélère encore les choses en production, mais impose de vider le cache OPcache explicitement à chaque déploiement (sinon l’ancien code reste servi) — une étape à automatiser dans le pipeline de déploiement.- La plupart des ORM (Doctrine, Eloquent) proposent un mécanisme d’eager loading (
with()en Eloquent,JOIN/fetch en Doctrine) qui résout le N+1 en une seule requête ou un nombre fixe de requêtes, indépendant du nombre d’éléments affichés. - Le cache applicatif (Redis, Memcached) et OPcache répondent à des problèmes différents : OPcache évite de recompiler du code, un cache applicatif évite de recalculer ou re-récupérer une donnée coûteuse (résultat de requête, appel API externe). Les deux sont complémentaires, pas interchangeables.
- Une optimisation qui complexifie le code sans gain mesurable n’est pas une optimisation : mesurer avant/après avec les mêmes conditions de charge est la seule façon de valider qu’un changement a réellement amélioré les performances.
🐻 À retenir
- ●OPcache met en cache le bytecode compilé des fichiers PHP, pas le résultat des requêtes ou des calculs : il évite de re-parser et re-compiler le même code à chaque requête HTTP.
- ●Le problème N+1 est presque toujours invisible dans le code, visible uniquement dans les logs SQL ou un profiler : une boucle propre en apparence peut cacher des centaines de requêtes.
- ●Ne jamais optimiser sans avoir mesuré d’abord : l’intuition sur où se trouve le goulot d’étranglement est fausse plus souvent qu’on ne le pense.