Table des matières :
- ESI (Edge Side Includes) dans Varnish : du “hole punching” maîtrisé, pas un cache magique
- Pré-requis : architecture reverse-proxy propre et séparation des responsabilités (PrestaShop 8.1/9, PHP 8.2+)
- Découper une page PrestaShop en fragments cachables : quoi fragmenter, et pourquoi
- VCL Varnish 7.x : activer ESI, gérer la compression, stabiliser la clé de cache
- Côté PrestaShop : produire des balises <esi:include> en Smarty et exposer des endpoints de fragments
- TTL, invalidation et “grace” : éviter les thundering herds et les purges explosives
- Debug et mesure : valider ESI, hit ratio et impact réel sur CWV
ESI (Edge Side Includes) dans Varnish : du “hole punching” maîtrisé, pas un cache magique
ESI (Edge Side Includes) est un mécanisme de composition de page côté cache : vous mettez en cache un HTML “global” (catégorie, produit, CMS…), puis vous laissez Varnish remplacer des zones ciblées (panier, compte, recommandations, bannière A/B…) via des sous-requêtes juste avant l’envoi de la réponse au client. Concrètement, le HTML contient des balises du type <esi:include src="/…"/> et Varnish exécute ces inclusions avant de renvoyer la réponse.
Sur PrestaShop, l’intérêt est surtout de sortir du cache des fragments réellement variables sans dégrader la cacheabilité du reste. Le cœur PrestaShop “pollue” facilement les pages avec des cookies et des éléments dépendants de session (cart, tokens, blocs modules), ce qui fait basculer un reverse-proxy en pass si vous ne normalisez pas. ESI est une stratégie réaliste quand vous avez des pages à fort trafic (PLP/PDP) et quelques composants qui empêchent le cache full-page.
Deux points à bien avoir en tête (sinon ESI “déçoit”) :
- ESI ne rend pas une page dynamique “gratuite” : si vos fragments appellent un backend lent, vous allez surtout déplacer le temps d’attente (et parfois le rendre plus visible, notamment sur le header).
- ESI est un outil de découpage, pas un moteur de personnalisation 1:1. Plus vous rendez des fragments dépendants de cookies utilisateur, plus vous recréez une explosion de variantes et donc un cache inefficace.
Limites à assumer : ESI augmente le nombre de requêtes backend (une page = 1 requête + N fragments), donc vous devez maîtriser TTL, grace, la cacheabilité des fragments, et éviter de transformer Varnish en simple “proxy fan-out”. Une bonne règle empirique : si la page “shell” passe en HIT mais que vous multipliez les fragments pass coûteux, vous améliorez le TTFB… sans réduire la charge PHP (voire en l’augmentant).
Enfin, côté SEO et rendu : ESI est exécuté serveur → client, donc les moteurs voient une page complète (pas du contenu “injecté JS”), à condition que vos fragments ne dépendent pas d’appels bloqués/erreurs backend. Le paramètre onerror="continue" sur les includes est utile pour éviter qu’un fragment non critique (ex. recommandations) casse le rendu global.
Pré-requis : architecture reverse-proxy propre et séparation des responsabilités (PrestaShop 8.1/9, PHP 8.2+)
Contexte versions : les exemples ci-dessous ciblent Varnish 7.x (VCL 4.1) et une boutique PrestaShop 8.1/9.x (front Smarty) sur PHP 8.2+. PrestaShop 9 s’appuie sur Symfony 6.4 côté Back-Office et de plus en plus de briques partagées, mais le front reste majoritairement Smarty ; ça a un impact direct sur la façon d’injecter les balises ESI. Pour un aperçu des changements perf/stack côté PS9, voir : PrestaShop 9 : nouveautés techniques Symfony 6.4, API et performances.
Sur l’architecture : Varnish doit être placé devant l’origin PHP (Apache/Nginx + PHP-FPM). Comme Varnish ne termine pas TLS “proprement” en natif, vous avez en général un frontal TLS (Nginx/HAProxy/Hitch) → Varnish → origin. Deux détails pratiques qui évitent des bugs “fantômes” :
- faites transiter un
X-Forwarded-Protocohérent (sinon redirections/URLs canoniques bancales, cookiesSecuremal posés, mixed content) ; - clarifiez qui gère la compression (backend, Varnish, ou les deux) — c’est particulièrement important avec ESI.
Important : si vous avez déjà un CDN, clarifiez qui fait quoi. ESI au niveau CDN est rarement compatible avec votre implémentation locale, donc vous choisissez : ESI côté edge CDN, ou ESI côté Varnish “interne”. Les deux en même temps = débogage pénible (et souvent un enchaînement de politiques de cache contradictoires : Vary, cookies, compression, headers de surrogate).
Côté PrestaShop, la séparation doit être stricte :
- Pages publiques (catégorie, produit, CMS) : cacheables, idéalement sans cookie.
- Fragments utilisateurs (panier, compte, wishlist, prix personnalisés B2B…) : rendus via ESI et souvent non-cacheables (ou cache très court + règles strictes).
- Endpoints AJAX/API : restent gérés comme d’habitude, mais attention à ne pas réintroduire des cookies sur des pages censées être “anonymous cache”.
Dans un contexte e-commerce francophone (FR/BE/CH), on a fréquemment plusieurs langues, devises et règles de TVA : c’est précisément le genre de “variantes structurantes” qu’il faut intégrer proprement dans la clé de cache (via headers dédiés ou un Vary maîtrisé), sans tomber dans le piège “je varie sur tout le cookie PrestaShop”.
Découper une page PrestaShop en fragments cachables : quoi fragmenter, et pourquoi
La règle opérationnelle : on fragmentera ce qui varie à haute fréquence ou par utilisateur, et on gardera en full-page cache ce qui est stable à l’échelle d’une langue/devise/groupe client. Sur une PLP (listing catégorie), les suspects classiques sont : mini-panier (quantité), bloc “Bonjour {prénom}”, menus dépendant du login, et parfois les prix si vous avez une logique client/segment (B2B, remises par groupe, prix par devise).
Un découpage ESI réaliste en PrestaShop :
- Header : logo/menu global cacheable, mais zone compte + panier en ESI.
- Product page : contenu cacheable, mais bloc disponibilité stock temps réel (si ERP) en ESI.
- Footer : souvent cacheable, mais blocs “vu récemment” et recommandations en ESI.
Pour décider “quoi fragmenter”, une grille simple (à adapter à votre boutique) :
| Fragment | Dépend de l’utilisateur ? | Change souvent ? | Politique conseillée |
|---|---|---|---|
| Mini-panier (quantité, total) | Oui | Oui | pass + Cache-Control: private, no-store |
| Bloc “Mon compte / Se connecter” | Oui | Faible | pass (ou cache très court si vous segmentez anonyme vs loggé) |
| Menu / catégories | Non | Faible | Cacheable (TTL long) |
| Prix (B2C standard) | Non (si pas de segmentation) | Moyen | Cacheable (TTL moyen + purge produit) |
| Prix B2B / remises par groupe | Par groupe (pas par client) | Moyen | Cacheable par groupe (clé dédiée) |
| Stock “temps réel” (ERP) | Non (souvent) | Élevé | ESI + TTL court (ex. 10–30 s) ou pass |
| Recommandations | Par session parfois | Moyen | ESI + TTL court, ou fallback si backend indispo |
Mini-scenario très courant : pendant un pic (opération commerciale, campagne TV, “soldes”), la PDP reçoit beaucoup de trafic anonyme. Si la page est full-pass à cause d’un cookie panier ou d’un bloc “compte”, le PHP prend tout. En passant en “shell cacheable + 2 fragments ESI”, vous absorbez l’essentiel des requêtes sur Varnish et vous réduisez la variance (ce qui compte souvent plus que le temps moyen). Mais attention à l’anti-pattern : un mini-panier ESI qui déclenche des calculs de livraison, règles de promo, et 20 requêtes SQL — il peut devenir le nouveau goulot.
Exemple concret (typique, pas universel) : une boutique avec TTFB PHP ~700–1200 ms sur PDP en charge (hooks modules + requêtes SQL), et une hit ratio Varnish faible (<20%) à cause des cookies. En passant en stratégie “page cache + ESI user blocks”, on vise souvent un TTFB côté client <100 ms sur les hits Varnish, tout en laissant 2–4 fragments non cacheables appeler l’origin. Le gain net dépend du poids back du fragment : si votre mini-panier exécute 20 requêtes SQL, vous n’avez rien “optimisé”, vous avez juste déplacé la douleur (et parfois multiplié le nombre total d’exécutions PHP).
VCL Varnish 7.x : activer ESI, gérer la compression, stabiliser la clé de cache
Varnish ne “devine” pas qu’il doit parser des ESI : il faut activer explicitement le traitement ESI côté vcl_backend_response (via beresp.do_esi). La manière la plus robuste consiste à signaler les réponses contenant des balises ESI via un header émis par l’origin (souvent Surrogate-Control) et n’activer do_esi que dans ce cas. Documentation : documentation Varnish (prendre la version correspondant à votre 7.x).
Point non négociable : la clé de cache. PrestaShop met beaucoup d’état dans des cookies (langue, devise, panier, session). Si vous laissez tout passer, aucune page n’est cacheable. La stratégie standard : 1) extraire les valeurs réellement structurantes (langue/devise/groupe) vers des headers “propres”, 2) supprimer les cookies sur les pages publiques, 3) hasher explicitement ces headers.
Autre point souvent oublié avec ESI : la compression. Si votre backend envoie du HTML déjà gzipé, Varnish ne peut pas parser les balises ESI telles quelles ; il faut soit désactiver gzip côté origin pour ces pages, soit demander à Varnish de décompresser avant traitement (et éventuellement de recomprimer à la fin). En VCL, cela se gère avec beresp.do_gunzip et beresp.do_gzip.
Exemple VCL (à adapter, testé conceptuellement sur Varnish 7.x / VCL 4.1). Risque : une mauvaise normalisation de cookie = fuite de contenu (langue/devise, voire compte). Testez en staging et ajoutez des assertions via varnishlog.
vcl 4.1;
import std;
backend default {
.host = "127.0.0.1";
.port = "8080";
}
sub vcl_recv {
# Sécuriser PURGE : uniquement depuis IPs internes
if (req.method == "PURGE") {
if (client.ip !~ 192.0.2.0/24 && client.ip != "127.0.0.1") {
return (synth(403, "PURGE interdit"));
}
# Implémentez ici la logique de purge adaptée (ban, surrogate keys, etc.)
return (synth(200, "PURGE accepté"));
}
# Ne jamais cacher si Authorization (API, Basic Auth, etc.)
if (req.http.Authorization) {
return (pass);
}
# Normalisation URL (évite fragmentation par querystring désordonnée)
set req.url = std.querysort(req.url);
# Fragments ESI : selon votre choix, soit pass (personnalisé), soit cacheable court
if (req.url ~ "^/esi/") {
return (pass);
}
# Extraire langue/devise depuis cookies PrestaShop
# IMPORTANT : ne faites pas un regsub "aveugle" (sinon valeur = cookie complet en cas de non-match)
if (req.http.Cookie) {
if (req.http.Cookie ~ "(^|;\s*)id_lang=") {
set req.http.X-PS-Lang = regsub(req.http.Cookie, ".*?(^|;\s*)id_lang=([^;]+);*.*", "\2");
}
if (req.http.Cookie ~ "(^|;\s*)id_currency=") {
set req.http.X-PS-Currency = regsub(req.http.Cookie, ".*?(^|;\s*)id_currency=([^;]+);*.*", "\2");
}
}
# Pages publiques : supprimer les cookies (sinon cache impossible)
# À ADAPTER à vos routes SEO PrestaShop : l'idée est d'exclure tout ce qui est compte/commande/panier/module
if (req.method == "GET"
&& req.url !~ "^/(admin|connexion|login|mon-compte|panier|commande|order|cart|module|api)(/|$)") {
unset req.http.Cookie;
}
return (hash);
}
sub vcl_hash {
hash_data(req.http.host);
hash_data(req.url);
# Variantes structurantes (ne mettez pas ici des identifiants utilisateur)
if (req.http.X-PS-Lang) { hash_data("lang=" + req.http.X-PS-Lang); }
if (req.http.X-PS-Currency) { hash_data("cur=" + req.http.X-PS-Currency); }
}
sub vcl_backend_response {
# Ne jamais mettre en cache si l'origin pose un Set-Cookie (signal fort d'état utilisateur)
if (beresp.http.Set-Cookie) {
set beresp.uncacheable = true;
return (deliver);
}
# Activer ESI uniquement sur les HTML marqués
if (beresp.http.Content-Type ~ "text/html"
&& beresp.http.Surrogate-Control ~ "ESI/1.0") {
set beresp.do_esi = true;
# Si l'origin compresse, demander à Varnish de décompresser avant parsing ESI
set beresp.do_gunzip = true;
}
# Recompression (optionnelle) : utile si vous décompressez pour ESI
set beresp.do_gzip = true;
# TTL/grace (à aligner avec votre stratégie)
if (beresp.ttl <= 0s) {
set beresp.ttl = 120s;
}
set beresp.grace = 30s;
return (deliver);
}
sub vcl_deliver {
if (obj.hits > 0) {
set resp.http.X-Cache = "HIT";
} else {
set resp.http.X-Cache = "MISS";
}
}
Deux recommandations “terrain” pour stabiliser votre hit ratio en production :
- Contrôlez
Vary: si l’origin renvoieVary: CookieouVary: User-Agentpar défaut, vous cassez la mutualisation. Sur des pages publiques, préférez des variantes explicites (langue/devise) via clé de hash, pas viaVarytrop large. - Limitez la profondeur ESI et le nombre de fragments : Varnish dispose de paramètres (par ex. profondeur max ESI). Même sans toucher aux paramètres, limitez-vous à quelques fragments critiques (2 à 5) ; au-delà, vous “micro-découpez” sans gain net.
Côté PrestaShop : produire des balises <esi:include> en Smarty et exposer des endpoints de fragments
PrestaShop ne fournit pas un pipeline ESI en standard. Il faut donc : (1) injecter des balises ESI dans les templates, (2) créer des endpoints qui rendent les fragments, (3) contrôler finement les headers. Dans un thème Smarty, vous pouvez injecter un include ESI dans header.tpl sur la zone mini-panier, en veillant à ne pas échapper les chevrons. En pratique, on encapsule souvent dans {literal} pour éviter que Smarty “nettoie” :
{* header.tpl *}
<div class="header-cart">
{literal}
<esi:include src="/esi/cart" onerror="continue" />
{/literal}
</div>
Les endpoints /esi/... doivent être prévisibles et minimaux. Implémentez-les via un module avec un front controller (PrestaShop 8.1/9) et retournez uniquement le HTML nécessaire (pas de layout, pas de JS global). Un fragment “propre” doit idéalement :
- rendre un HTML autonome (le strict nécessaire, pas une page complète) ;
- être rapide (sinon il devient le nouveau bottleneck) ;
- avoir une politique de cache explicite (
Cache-Control, éventuellementSurrogate-Control) ; - être testé en isolement (y compris multi-utilisateurs et multi-langues).
Pour un fragment utilisateur (panier), évitez de le rendre cacheable publiquement : utilisez Cache-Control: private, no-store et/ou forcez pass côté Varnish (comme dans le VCL plus haut). Si le fragment est commun (ex. bannière top-bar dépendant seulement de la langue), vous pouvez le rendre public, s-maxage=300 et l’inclure en ESI cacheable (ce qui réduit encore les appels origin).
Exemple (simplifié) côté contrôleur module :
// modules/monesi/controllers/front/cart.php
class MonEsiCartModuleFrontController extends ModuleFrontController
{
public function initContent()
{
parent::initContent();
// Rendu HTML minimal
$this->setTemplate('module:monesi/views/templates/front/cart-fragment.tpl');
// Fragment utilisateur => non cacheable
header('Cache-Control: private, no-store');
}
}
Pour que Varnish sache que la page “parent” contient de l’ESI, marquez la réponse HTML principale. Le plus simple est d’ajouter Surrogate-Control: content="ESI/1.0" sur les pages qui contiennent des tags ESI (ex. via un override de FrontController, ou un hook global du thème/module). Point d’attention : gardez ce marquage au plus près des pages concernées. Mettre Surrogate-Control partout “au cas où” peut activer ESI sur des pages où ce n’est pas utile (coût CPU inutile + complexité).
Avec PrestaShop 9, vous pouvez aussi vous brancher plus proprement sur la couche Symfony quand vous rendez via des contrôleurs Symfony, mais sur le front historique, vous êtes vite sur du legacy/override : documentez-le, testez-le, et acceptez que ce soit fragile (notamment lors de mises à jour majeures). Pour anticiper ces impacts de stack, le billet sur les évolutions techniques de PrestaShop 9 est utile : zoom sur les nouveautés Symfony/API/perf de PrestaShop 9.
TTL, invalidation et “grace” : éviter les thundering herds et les purges explosives
ESI ne remplace pas une stratégie TTL/invalidation ; il la rend plus critique. Objectif : TTL long sur le “shell” (page cacheable) et TTL plus court ou pass sur les fragments. Si vous invalidez tout le shell à chaque update stock/prix, vous perdez l’intérêt. À l’inverse, si vous laissez un shell trop longtemps sans invalidation, vous risquez d’afficher des infos périmées (ex. prix, dispo).
Une stratégie simple et souvent efficace (à adapter) :
- Shell PDP/PLP :
s-maxage5 à 15 min (ou TTL Varnish équivalent), avecgrace30–120 s. - Fragments “temps réel” (stock, livraison estimée) : ESI avec TTL 10–60 s, ou
passsi réellement user-specific. - Fragments user (panier, compte) :
private, no-store+pass(pas de débat).
Deux mécanismes HTTP utiles : stale-while-revalidate (RFC 5861) et grace côté Varnish. stale-while-revalidate formalise le fait de servir temporairement une réponse expirée pendant qu’on revalide en arrière-plan ; Varnish propose un mécanisme analogue (et très utilisé en prod) via beresp.grace. Référence utile : RFC 5861.
Pour l’invalidation : sur PrestaShop, les événements pertinents sont souvent “produit mis à jour”, “prix modifié”, “catégorie réordonnée”, “CMS modifié”. Le core n’émet pas une politique de purge “reverse-proxy friendly” par défaut. En pratique, vous finissez avec :
- des PURGE ciblés par URL (simple, mais brut),
- ou des BAN par pattern (dangereux si trop large),
- ou des surrogate keys (nécessite design + éventuellement modules Varnish).
Si vous ne voulez pas bricoler de surrogate keys tout de suite, assumez un TTL modéré (ex. 5–15 min) pour les pages produit/catégorie + invalidation ciblée lors d’opérations critiques (maj prix/stock). Si votre stock est en temps réel (ERP), ne cherchez pas à invalider le shell à chaque mouvement : mettez plutôt la disponibilité en fragment ESI (court TTL ou pass). Cette logique se marie bien avec des architectures type inventaire unifié / routage par canal (cf. inventaire unifié : synchro temps réel et routage).
Checklist anti-“purges explosives” (simple mais efficace) :
- purger le fragment (stock) plutôt que le shell, quand c’est possible ;
- privilégier des TTL raisonnables +
graceplutôt qu’un TTL très long + purge permanente ; - limiter les BAN “regex larges” (elles peuvent invalider massivement et dégrader brutalement le hit ratio) ;
- en cas de multiples langues/devise, vérifier que la purge touche bien toutes les variantes (ou au contraire uniquement celles concernées).
Debug et mesure : valider ESI, hit ratio et impact réel sur CWV
Déboguer ESI “à l’aveugle” est une perte de temps. Activez des headers (X-Cache, X-Cache-Hits, éventuellement un marqueur côté fragments) et utilisez varnishlog pour vérifier : (1) que la page shell est un HIT, (2) que Varnish exécute les sous-requêtes ESI, (3) que les fragments ne sont pas mis en cache par accident.
Commandes utiles :
varnishlog -g request -q 'ReqURL ~ "^/esi/"'pour observer les fragments,varnishstatpour suivreMAIN.cache_hit,MAIN.cache_miss,MAIN.esi_errors,varnishlog -g request -q 'RespHeader ~ "X-Cache: HIT"'pour isoler les hits et vérifier le comportement en charge.
Côté navigateur, mesurez avant/après : TTFB, LCP, INP, et surtout la variance en charge. Une page servie en HIT Varnish améliore souvent LCP via un HTML plus rapide, mais ESI peut aussi dégrader si vos fragments sont lents et bloquants (header rendu tardif, layout shift). Dans la pratique, vous voulez éviter deux pièges :
- un fragment critique (header/panier) qui arrive tard et provoque un “saut” visuel (CLS) ;
- un fragment qui force des recalculs côté client (beaucoup de JS) et pénalise INP.
Pour une méthodologie propre (Waterfall, CPU throttling, cache states), appuyez-vous sur : méthodes pro avec DevTools pour mesurer/optimiser les performances web et pour relier vos choix cache/ESI à des actions concrètes : plan d’action Core Web Vitals (LCP, INP, CLS).
Enfin, traitez la sécurité comme un prérequis, pas une option. Un mauvais découpage ESI peut divulguer des données (ex. fragment compte mis en cache public). Ajoutez des garde-fous :
Cache-Controlexplicite sur les fragments (et cohérent avec votre VCL),- blocage
Set-Cookiecôté Varnish pour les objets supposés cacheables, - tests d’isolement multi-utilisateurs (navigateur A/B, sessions distinctes, langues/devises différentes),
- revue des endpoints
/esi/(pas d’accès à des infos sensibles sans contrôle).
Pour un cadre de menaces orienté PrestaShop (tokens, sessions, headers, proxies), voir : Sécurité PrestaShop 2026 : risques majeurs et protections professionnelles.




