Une page de listing à 3,5 secondes, chez un client e-commerce. L’équipe la trouvait « un peu lente » depuis des mois, sans jamais mettre le doigt sur la cause. Le code de la page était lisible. Rien ne sautait aux yeux. En comptant les requêtes, le verdict est tombé en deux minutes : un N+1 sur les images produit. Un with() ajouté au bon endroit, la page est repassée sous 400 ms. Vingt minutes de travail, café compris.
Ce scénario, je le rejoue à l’identique plusieurs fois par an. C’est de loin la lenteur que je rencontre le plus souvent, et elle se planque toujours derrière le confort d’un ORM.
Le N+1, c’est quand une appli tire une requête par ligne au lieu d’une seule pour afficher une liste : on le démasque en comptant les requêtes par page, et le chargement anticipé ramène souvent la page sous le demi-seconde.
Ce qui se passe vraiment sous le capot
Vous affichez 100 commandes, et pour chacune le nom du client. Le code paraît anodin : on récupère les commandes, et dans la boucle on lit le client de chaque ligne. Mais l’ORM, lui, tire 1 requête pour les commandes, puis 1 requête par commande pour aller chercher le client. Résultat : 101 requêtes là où 2 auraient suffi.
D’où le nom : 1 requête de départ, plus N requêtes derrière, une par élément. Sur une page de démo, ça passe inaperçu. Sous trafic réel, c’est une base qui sature sans qu’on comprenne d’où vient la pression.
Le mettre en évidence
Le symptôme se devine à l’œil : plus la liste s’allonge, plus la page traîne. Reste à le prouver, et ça, ça se compte.
La plupart des stacks offrent de quoi le faire sans rien installer de lourd. Django Debug Toolbar, le query log de Laravel, le profiler de Symfony : peu importe l’outil, le geste est le même. Vous ouvrez la page, vous lisez le compteur de requêtes. 150 requêtes pour une simple liste ? Le diagnostic est posé.
Le correctif : le chargement anticipé
On dit à l’ORM de charger les relations en amont, en une requête groupée, au lieu de les réclamer une par une dans la boucle. C’est ce qu’on appelle le chargement anticipé, ou eager loading. Vos 101 requêtes redeviennent 2, et l’effet sur le temps de réponse est souvent spectaculaire.
Concrètement, c’est with() côté Laravel, select_related et prefetch_related côté Django, le fetch join côté Doctrine. Des syntaxes différentes, une seule idée. Vous prévenez l’ORM des relations dont vous aurez besoin, pour qu’il aille les chercher d’un bloc.
Là où il faut rester mesuré
Ce n’est pas un interrupteur qu’on laisse sur ON partout. Charger toutes les relations « au cas où » ramène des données que personne n’affiche et déplace simplement le coût ailleurs. Je m’en tiens aux relations réellement utilisées par la page, pas à l’arbre complet du modèle.
Une réserve, pour finir
Compter les requêtes vous dit qu’il y a un N+1 et où regarder. Ça ne vous dit pas toujours quoi charger. Sur une page qui agrège une dizaine de relations imbriquées, le bon réglage tient parfois plus de l’ajustement que de la règle : on charge, on remesure, on retire ce qui ne sert pas. La méthode démasque le problème de façon fiable. La correction fine, elle, demande de connaître ce que la page affiche vraiment, et aucun compteur ne le sait à votre place.