Optimisation de code PrestaShop : diagnostiquer lenteurs et requêtes SQL

Guide pratique pour diagnostiquer et corriger les lenteurs PrestaShop : TTFB élevé, profilage PHP, requêtes lentes (slow log), antipatterns N+1, indexation et cache Redis.

Écrans affichant du code et des graphiques pour l'optimisation SQL PrestaShop.

Table des matières :

  1. Isoler la latence : HTTP/TTFB, PHP-FPM et MySQL, sans se raconter d’histoires
  2. Profiler côté PrestaShop/PHP : compter les requêtes, trouver les hooks toxiques, mesurer le CPU
  3. Capturer les requêtes lentes : slow query log, agrégation, puis EXPLAIN (vraiment)
  4. Anti-patterns SQL fréquents en PrestaShop : N+1, facettes, multiboutique et surcharge ObjectModel
  5. Réparer proprement : indexes, refactoring ciblé, caches (Redis), et garde-fous de déploiement
  6. Méthode opérationnelle en 60–90 minutes : de “ça rame” à un plan d’action chiffré

La plupart des lenteurs PrestaShop ne viennent pas d’“un site lent” mais d’un chemin de requête précis : une route (front ou BO), un hook, une requête SQL, une boucle PHP, ou un verrou InnoDB. L’objectif n’est pas de “tout optimiser”, mais d’identifier la poignée de points chauds qui dégradent le TTFB, saturent la base, ou font exploser le temps CPU.

Un bon diagnostic se lit rarement sur une moyenne : sur une boutique e-commerce, ce sont les percentiles (P95/P99) qui révèlent les pics (promos, batchs, modules qui s’exécutent “parfois”). C’est aussi ce qui vous permet de dire “ça s’améliore” sans débat : même URL, même contexte, même protocole de mesure, avant/après.

Isoler la latence : HTTP/TTFB, PHP-FPM et MySQL, sans se raconter d’histoires

Commencez par une segmentation factuelle des temps : réseau/TLS, serveur web (Nginx/Apache/Caddy), exécution PHP, puis base MySQL/MariaDB. Sur un PrestaShop 8.1.x ou 9.x (PHP 8.2/8.3), la majorité des “pages lentes” se traduit côté client par un TTFB élevé, donc une latence serveur. Si vous n’avez pas un point de mesure fiable (logs d’accès avec $request_time, APM, traces), vous optimiserez au hasard.

Pour éviter les faux positifs, validez aussi d’où vous mesurez. Une boutique hébergée en France/UE, mais mesurée depuis un agent à l’étranger, peut afficher un TTFB perçu plus élevé à cause du réseau (RTT, peering), sans que PHP/MySQL soient réellement en cause. En pratique : faites au moins deux points de mesure (un proche du DC et un “client réel”), et conservez la même méthode tout au long du diagnostic.

Un tableau simple (et très utile en incident) :

Couche Signal typique Où regarder Hypothèse fréquente
Réseau/TLS latence “globale” qui varie, mais PHP stable logs reverse-proxy / CDN / métriques réseau saturation, handshake, I/O logs, throttling
Serveur web → PHP-FPM upstream_response_time haut Nginx/Apache, métriques PHP-FPM file d’attente, workers, CPU PHP
PHP → DB temps SQL cumulé dominant profiler PrestaShop, APM requêtes répétitives, indexes absents
DB/InnoDB dents de scie, blocages slow log, InnoDB status, waits/locks contention, batchs, verrous

Côté serveur web, activez une journalisation de temps côté reverse-proxy ou serveur web. Sur Nginx, le duo request_time (temps total) et upstream_response_time (temps PHP-FPM) permet de savoir si la latence vient du PHP ou d’un goulot (file d’attente, I/O, TLS). Un pattern fréquent : upstream_response_time bas mais request_time haut → problème réseau, throttling, ou écriture disque (logs, sessions) ; upstream_response_time haut → PHP ou SQL.

Si vous n’avez pas encore de log_format exploitable, voici une base (à adapter à votre conf) pour corréler URL, temps, upstream et taille :

log_format timed_combined '$remote_addr - $host "$request" '
  'status=$status bytes=$body_bytes_sent '
  'rt=$request_time urt=$upstream_response_time '
  'uaddr=$upstream_addr '
  'ref="$http_referer" ua="$http_user_agent"';

access_log /var/log/nginx/access_timed.log timed_combined;

Ensuite, n’analysez pas seulement “une fois”. Faites 10–20 hits sur la même URL (dans des conditions proches) et regardez les percentiles : un rt qui passe de 0,3 s à 3 s “parfois” est typique d’un lock, d’un cache qui se réchauffe, ou d’un module conditionnel.

Si vous êtes sur une archi moderne type workers, lisez aussi les impacts des modèles d’exécution : l’article sur FrankenPHP détaille les points d’attention côté stabilité et workers.

Côté MySQL/MariaDB, ne vous limitez pas à “CPU à 90%”. Mesurez les waits (I/O, locks) et la contention. Une boutique qui “rame” pendant des imports ou des recalculs d’index peut être bloquée par des verrous InnoDB plutôt que par une requête intrinsèquement lente. Les métriques à surveiller en priorité : latence P95/P99 des requêtes, Threads_running, Innodb_row_lock_time, IOPS disque, et taille/efficacité du buffer pool.

Mini-scenario (très courant) : un import produit (ou un connecteur ERP) déclenche des UPDATE/INSERT massifs pendant qu’un autre processus reconstruit un index (recherche, facettes, stocks). Résultat : vous observez des pages “aléatoirement” à 5–10 s, alors que les requêtes unitaires ne sont pas lentes — ce sont des attentes de verrous. Dans ce cas, optimiser un SELECT n’apportera rien tant que les écritures concurrentes ne sont pas séquencées.

Si votre socle base est bancal (moteur, collation, UTF-8, migration MySQL→MariaDB), corrigez d’abord : Prérequis système / migration MySQL vers MariaDB.

Profiler côté PrestaShop/PHP : compter les requêtes, trouver les hooks toxiques, mesurer le CPU

PrestaShop fournit des outils utiles, mais ils sont souvent mal utilisés (ou activés en prod sans garde-fou). Sur PrestaShop 8.1.x, le Debug Profiler permet d’afficher, pour une page donnée, le nombre de requêtes SQL, leur durée cumulée, et les plus coûteuses. C’est l’outil le plus rapide pour confirmer un diagnostic “SQL-bound”. La procédure, les captures et les points d’analyse sont détaillés ici : PrestaShop Debug Profiling.

Deux bonnes pratiques pour que ce profiler serve vraiment :

  • Comparer des pages comparables : même catégorie, même pagination, même devise/langue, même état de cache. Sinon, vous conclurez à tort qu’un module “ralentit” alors que vous avez changé le contexte (tri, facettes, cookies, etc.).
  • Regarder le ratio “temps SQL cumulé” vs “temps total” : si la page fait 1,8 s dont 1,4 s SQL, vous savez immédiatement où mettre l’effort. À l’inverse, une page à 1,8 s avec seulement 0,2 s SQL est plutôt un sujet PHP (boucles, rendu, I/O, API externes).

La limite du profiler PrestaShop : il donne une vue “surface” (liste de requêtes, timings), mais il ne vous dira pas quel morceau de code a déclenché la requête (souvent via ObjectModel, un hook, ou un module tiers). Pour remonter au call stack, vous avez deux options réalistes :

  • Xdebug 3 en environnement de dev/staging uniquement (l’overhead est massif en prod) : Xdebug 3 installation et configuration IDE : Xdebug VS Code configuration.
  • Un profiler de production type Blackfire (ou équivalent APM), avec échantillonnage et traces, si vous avez besoin d’observer le trafic réel.

Pour “trouver les hooks toxiques” sans partir dans tous les sens, une technique pragmatique consiste à lister les hooks qui s’exécutent sur toutes les pages (ex. displayHeader, actionDispatcher, actionFrontControllerSetMedia) et à vérifier si un module y fait :

  • des requêtes SQL non cachées ;
  • des appels HTTP sortants (tracking, API, etc.) ;
  • des opérations sur le filesystem (lecture de templates, de fichiers de config, logs verbeux).

C’est souvent là que se cachent les régressions : un module “marketing” ajouté pour une campagne peut toucher l’ensemble du trafic, pas seulement une page.

Gardez une règle simple (et toujours valable en 2026) : “Premature optimization is the root of all evil.” (Donald E. Knuth, Structured Programming with go to Statements, 1974). Sur PrestaShop, ça se traduit par : ne refactorez pas “au feeling” un Product::getProductProperties() parce qu’il vous semble moche ; refactorez parce que vous avez une trace montrant 35% CPU sur une boucle, ou 60% du temps dans des SELECT répétitifs.

Capturer les requêtes lentes : slow query log, agrégation, puis EXPLAIN (vraiment)

Le point de départ robust, c’est le slow query log MySQL/MariaDB. Il capture les requêtes dépassant un seuil (long_query_time) et/ou les requêtes non indexées selon config. Sur une boutique à trafic, vous voulez un seuil bas en pré-production (ex. 100–200 ms) pour attraper les “presque lentes”, et plus haut en prod (ex. 500–1000 ms) pour limiter le bruit. Guide d’activation et pièges (rotation, volumétrie, paramètres) : Slow query log guide.

Un point important en audit : une requête “lente” n’est pas uniquement une requête avec un Query_time élevé. Dans le slow log (selon config), vous pouvez aussi corréler :

  • Rows_examined très haut (scan large) ;
  • Rows_sent très bas (beaucoup de travail pour peu de résultat) ;
  • fréquence d’exécution (une “petite” requête répétée fait un “gros” problème).

Ne lisez pas le slow log “à l’œil”. Agrégez. L’outil classique reste pt-query-digest (Percona Toolkit) : pt-query-digest. Il normalise les requêtes, calcule les percentiles, et vous montre les “top offenders” par temps total (pas seulement par latence). C’est crucial sur PrestaShop, où une requête à 40 ms exécutée 10 000 fois peut faire plus de dégâts qu’une requête à 600 ms exécutée 5 fois.

Astuce opérationnelle : dans le rapport pt-query-digest, commencez par la section “Rank by total time”, puis vérifiez si le problème est :

  • un monstre rare (latence énorme, faible fréquence) → souvent lock, I/O, ou plan instable ;
  • une épine répétitive (latence moyenne, fréquence énorme) → typique N+1, hook, boucle de rendu, facettes.

Ensuite seulement, vous passez à EXPLAIN / EXPLAIN ANALYZE. Objectif : confirmer si la requête utilise un index pertinent, si elle scanne une table entière (ALL), si elle explose en rows examined, ou si elle fait des Using temporary; Using filesort.

Checklist rapide (qui évite 80% des erreurs d’interprétation) :

  • L’index utilisé correspond-il vraiment aux colonnes de WHERE dans le bon ordre (surtout pour les composites) ?
  • Le type (ref, range, ALL, etc.) est-il cohérent avec le volume attendu ?
  • Le rows estimé est-il crédible (sinon, statistiques obsolètes / distribution atypique) ?
  • La requête trie-t-elle (filesort) alors qu’un index pourrait porter le WHERE + ORDER BY ?

À ce stade, l’article “Développeur MySQL : optimiser requêtes, schémas et performances en production” est un complément direct (indexes composites, cardinalités, statistiques, buffering) : Développeur MySQL.

Anti-patterns SQL fréquents en PrestaShop : N+1, facettes, multiboutique et surcharge ObjectModel

Le classique : le N+1 queries. Sur une liste produit, vous récupérez N produits, puis pour chaque produit vous redemandez les features, les prix spécifiques, le stock, les images… La racine est souvent une mauvaise utilisation d’ObjectModel et de getters qui “semblent” simples mais déclenchent une requête à chaque appel. PrestaShop n’a pas d’ORM moderne pour l’historique legacy, et on retombe vite dans le pattern décrit dans cet article.

Le piège en audit : un N+1 peut être “invisible” sur un petit catalogue (20 produits) et exploser sur une catégorie à 200 produits, ou sur une recherche avec tri spécifique. Indice simple : beaucoup de requêtes semblables, seules les valeurs changent (idproduct, idattribute, etc.).

Correctif typique : regrouper les lectures en une ou deux requêtes set-based (JOIN / IN), puis hydrater en mémoire. Exemple simplifié (PrestaShop 8.1.x / PHP 8.2), au lieu de boucler sur Feature::getFeatureValueLang() :

$ids = array_map('intval', $productIds);
$sql = 'SELECT fp.id_product, fvl.value
        FROM '._DB_PREFIX_.'feature_product fp
        JOIN '._DB_PREFIX_.'feature_value_lang fvl
          ON fvl.id_feature_value = fp.id_feature_value
         AND fvl.id_lang = '.(int)$idLang.'
        WHERE fp.id_product IN ('.implode(',', $ids).')';
$rows = Db::getInstance()->executeS($sql);
// indexation en tableau [id_product] => [...]

Deux compléments “réalistes” côté PrestaShop :

  • Pensez au multiboutique : certaines tables ont une dimension *_shop. Une requête set-based doit filtrer correctement id_shop, sinon vous ramenez trop de lignes (et vous “optimisez” en aggravant).
  • Évitez de reconstruire des structures coûteuses à chaque appel : si vous indexez $rows en mémoire, faites-le une fois par requête, pas au sein d’une boucle de rendu.

Second point noir : ps_facetedsearch (navigation à facettes). Sur gros catalogues, les facettes font exploser le coût des requêtes (multi-joins, group by, filtres combinatoires), surtout si la couche d’index n’est pas propre, si la table de stock est joinée sans index adapté, ou si vous avez des combinaisons massives. Les lenteurs se voient côté UX, mais aussi côté SEO (crawl budget, latence).

Quelques leviers concrets (souvent plus efficaces qu’un “index miracle”) :

  • Réduire les facettes actives sur les catégories volumineuses (trop de dimensions = trop de combinaisons).
  • Vérifier que l’index facettes est reconstruit après import/maj (sinon, vous payez le coût sans bénéfice).
  • Sur certaines boutiques, désactiver des compteurs ou options “nice to have” peut faire gagner plus qu’un refactoring (car ce sont des GROUP BY coûteux).

Pour replacer ça dans une stratégie globale perf+SEO (CWV, pages catégories), l’article “PrestaShop performance : optimiser gros catalogues et Core Web Vitals” sert de socle : PrestaShop performance (CWV).

Troisième cas : multiboutique/multilang. Beaucoup de requêtes PrestaShop joignent *_lang et *_shop avec id_lang et id_shop. Si les indexes composites ne suivent pas, vous obtenez des scans inutiles. Pire : certains modules ajoutent des filtres dynamiques sur des champs non indexés (ou de la recherche LIKE '%...%' sur de gros champs), et la base tombe en I/O.

La vraie correction n’est pas “ajouter un index partout”, c’est d’aligner : WHERE + ORDER BY + JOIN columns + cardinalité + volume, puis de vérifier via EXPLAIN et mesures P95. Et si une recherche “contient” (%mot%) est un besoin produit, il faut parfois envisager un autre mécanisme (index plein texte, moteur dédié), plutôt que de forcer MySQL à scanner des champs longs.

Réparer proprement : indexes, refactoring ciblé, caches (Redis), et garde-fous de déploiement

Sur la partie SQL, la correction la plus rentable reste souvent un index composite bien choisi. Exemple concret (pattern fréquent) : une requête filtre sur (id_shop, id_lang, active) et ordonne par position. Un index (id_shop, id_lang, active, position) peut réduire drastiquement rows examined et supprimer un filesort.

Mais un “bon” index a aussi un coût (écriture plus lente, stockage, maintenance). Avant d’ajouter, validez :

  • est-ce une requête hot path (front, checkout, BO commandes) ou un écran rare ?
  • est-elle fréquente (temps total élevé) ou juste “parfois lente” ?
  • l’index est-il compatible avec les écritures (imports, updates stock) ?

Sur des tables volumineuses (commandes, logs, index de recherche), ajouter un index en prod peut bloquer si vous n’anticipez pas la fenêtre, la réplication, ou un outil de changement en ligne. Ne faites pas ça “à 14h un mardi” sur une boutique qui vend. Sur un contexte sensible (soldes, opérations marketing, périodes de forte volumétrie en France/Europe), la règle est simple : pas de DDL lourd sans plan de rollback.

Sur la partie PHP, le refactoring efficace est celui qui réduit la fréquence de déclenchement des requêtes. Les stratégies qui fonctionnent sur PrestaShop :

  • Précharger les données nécessaires en une passe (éviter les getters qui requêtent).
  • Réduire le travail fait dans les hooks (certains modules exécutent du SQL dans displayHeader ou actionDispatcher, donc sur toutes les pages).
  • Mettre en cache applicatif ce qui est stable (config, mappings, listes), avec invalidation contrôlée.

Pour le cache, Redis est le standard pragmatique sur PrestaShop dès que la charge augmente (cache, sessions selon stack, verrous applicatifs éventuels). Un cache mal invalidé est pire qu’une lenteur, mais un cache correctement scindé par id_shop, id_lang, currency et “context” est un accélérateur net.

Check rapide “anti-cache toxique” (utile en revue de code) :

  • La clé inclut-elle le contexte fonctionnel (langue, boutique, groupe client, devise) ?
  • Y a-t-il un TTL raisonnable si l’invalidation n’est pas fiable ?
  • En cas de cache manquant, le code fait-il une seule requête “grosse”, ou retombe-t-il dans un N+1 ?

Mise en place côté PrestaShop : Redis PrestaShop configuration (et côté extension PHP : Redis PHP extension). En parallèle, vérifiez OPcache : une boutique qui “rame” après déploiement peut simplement souffrir d’une config OPcache trop faible ou d’un reset mal géré : PHP OPcache recommended settings.

Enfin, industrialisez : benchmark avant/après, tests de non-régression, et monitoring continu. Sur PrestaShop, une optimisation qui “marche en staging” mais casse la prod arrive vite, surtout si vous touchez au SQL ou aux caches. Travaillez avec une pré-production fidèle et une checklist de sauvegarde/rollback (utile même hors mise à jour) : mise à jour checklist. Et pour éviter de découvrir les régressions par les clients, mettez en place une surveillance des erreurs et des latences (PHP, MySQL, JS) avec alertes : PrestaShop monitoring article.

Méthode opérationnelle en 60–90 minutes : de “ça rame” à un plan d’action chiffré

Si vous devez livrer un diagnostic rapide (CTO pressé, soldes, incident), imposez une routine courte. 1) Identifiez 2–3 URLs critiques (catégorie, produit, panier/checkout, BO commande). 2) Mesurez TTFB et temps serveur avec 10–20 hits pour obtenir un P95 (pas une moyenne). 3) Activez temporairement le profiling PrestaShop en environnement contrôlé, ou utilisez une capture APM si disponible, pour obtenir : nombre de requêtes, top 10 des requêtes par durée, et top fonctions PHP par CPU.

Pour que ce “60–90 minutes” aboutisse à une décision, forcez un livrable minimal, même sur un incident :

Élément Attendu Exemple de sortie
Mesure initiale P95 + contexte “Catégorie X : P95 TTFB 2,8 s (20 hits), cache ON, FR desktop”
Evidence 1–3 preuves top requêtes slow log + capture profiler
Hypothèse une cause principale N+1 dans module / index manquant / lock import
Action une action testable index composite + PR refactoring + cache
Vérification même protocole P95 repassé à 0,8 s, requêtes -60%

Ensuite, classez les problèmes par levier :

  • SQL dominant : temps cumulé SQL > 50% et quelques requêtes top lourdes → slow log + EXPLAIN + indexes/refactoring set-based.
  • PHP dominant : temps CPU PHP haut avec peu de SQL → profiler (Xdebug/Blackfire) + micro-refactoring (boucles, sérialisation, parsing), et vérif OPcache.
  • Contention/locks : latence en dents de scie, Threads_running élevé, imports parallèles → analyse locks InnoDB, séquencement des batchs, baisse de concurrence, éventuellement séparation des workloads.

Terminez par un plan d’action chiffré et testable. Exemple réaliste observé en boutique avec gros catalogue : page catégorie P95 à 2,8 s, 220 requêtes SQL. Après (1) suppression d’un N+1 dans un module de badges produit, (2) index composite sur une table *_lang utilisée dans le tri, (3) cache Redis sur un mapping stable, P95 à 0,75 s, 70 requêtes SQL. Le gain n’est pas “magique” : il est mesuré, attribuable, et réversible. Si vous n’êtes pas capable de fournir ces trois propriétés (mesuré/attribuable/réversible), votre “optimisation de code PrestaShop” est juste un pari.


À lire aussi