La mise en cache de pages web nécessitant des informations de session (exemple : gestion d’un panier de commande) présente quelques subtilités. Cet article a pour objectif de décrire plusieurs solutions envisageables.
Solution 1 : Page complète en cache et appel Ajax sans cache
La page posant un problème de charge serveur est entièrement cachée, sans informations liées à la session utilisateur (sinon GROS problèmes de sécurité).
Cette page contient dans son body HTML un appel Ajax vers une page non cachée et contenant simplement les blocs HTML utilisant des informations liées à la session utilisateur (de préférence sans appel à une base de données ou autre source de données pouvant ralentir la génération de la page).
La réponse de l’appel Ajax doit aussi contenir du code JavaScript permettant de rajouter les blocs HTML dans la page complète.
Avantages de cette solution :
- Une multitude de frameworks JavaScript permettent de mettre en oeuvre facilement cette solution (cf require.js, backbone.js, et bien d’autres…) ;
- Seuls les blocs utilisant la session ne sont pas cachés, ce qui allège le chargement de données non cachées.
Inconvénients :
- Cette solution double le nombre d’appels à la plateforme. Toutefois, sur les deux appels, seul l’appel Ajax arrive sur le serveur exécutant PHP, l’autre s’arrêtera au serveur de cache (Varnish) tant que celui-ci est frais ;
- Avant construction des blocs contenant les informations de session, la page n’affiche pas ces blocs, on peut donc avoir un effet de scintillement de l’interface. Plus problématique, en cas d’erreur du webservice, ces blocs ne seront jamais affichés.
Variante possible : utilisation de JSONP + templates
L’appel Ajax peut se faire en JSONP, ce qui permet à la page complète de gérer le templating des blocs HTML en remplacant simplement les valeurs trouvées dans le JSON renvoyé par le webservice.
Rappel du JSONP : un webservice est appelé dans une balise <script> placée dans le <head> de la page complète. L’appel à ce webservice contient en paramètre le nom de la fonction javascript qui sera appelée à la fin du chargement de la réponse.
Le processus est alors un peu différent :
- La page complète est chargée, avec du cache et sans données de session ;
- Une fois la page chargée, on lance l’appel JSONP sur un webservice avec un paramètre GET « jsonp » contenant le nom d’une fonction JavaScript existante ;
- Le webservice renvoit les données utilisateur au format JSON en paramètres de l’appel à la fonction JavaScript ;
- La fonction JavaScript utilise les données du JSON pour compiler les templates des blocs HTML et les ajouter dans le body de la page ;
- La page est maintenant complète avec les blocs HTML utilisant les informations de session.
Avantages de cette variante :
- Templates faciles à maintenir, utilisation possible d’un modèle MVC JavaScript ;
- Les templates seront mis en cache avec la page complète, ce qui diminue la bande passante nécessaire dans la plupart des cas ;
- En option, on peut aussi charger les templates par appels Ajax, avec un temps de cache potentiellement plus long. Veiller quand même à ce que le template ET les données soient bien chargés AVANT de compiler le template ;
- Le webservice ne renvoit que des données, il est donc envisageable de les déporter sur un serveur Node.js pour gagner en performance.
Inconvénients :
- Demande un traitement supplémentaire côté client : la compilation des templates.
Solution 2 : Utilisation des ESI (cache varnish)
L’utilisation des ESI permet de cacher des blocs HTML d’une page avec des stratégies différentes. Cela se traduit par l’ajout d’une balise <esi /> dans le code HTML de la page.
Ces balises seront traitées par le serveur de cache (Varnish) de la façon suivante :
- Prise en compte des paramètres de cache de la page contenant la balise <esi /> et récupération de son code HTML (soit en cache, soit en rafraichissant le cache) ;
- Vérification de la présence de balises <esi /> et prise en compte de leurs paramètres de cache + chargement du contenu du bloc ;
- Vérification de la présence de balises <esi /> dans le bloc, etc de façon récursive.
A chaque balise <esi /> doit correspond une URL permettant de rafraichir le bloc.
Exemple :
<!DOCTYPE html><html> <body> <!-- ... some content --> <!-- Embed the content of another page here --> <esi:include src="http://..." /> <!-- ... some content --> </body></html>
Certains frameworks de développement permettent de faciliter l’intégration des ESI. Ainsi dans le cas de Symfony 2 et de Twig, l’intégration des ESI on peut simplement définir un ESI sur une action d’un contrôleur, une route privée sera alors générée par Symfony.
Exemple avec Symfony 2.2 :
{% render '...:news' with {}, {'standalone': true} %}
Pour plus d’informations sur l’utlisation des ESI dans Symfony 2, cf http://symfony.com/fr/doc/master/book/http_cache.html#utiliser-esi-avec-symfony2
Pour plus d’informations sur l’utilisation des ESI de Varnish dans un projet Symfony 2 : http://symfony.com/fr/doc/master/cookbook/cache/varnish.html
Dans le cas d’un site web n’utilisant pas framework offrant ce type de mécanisme, il faut écrire des routes spécifiques pour chacun des blocs dont le cache est géré par ESI.
Avantages de cette solution :
- Permet non seulement de gérer différemment le cache sur les blocs utilisant des données de session, mais aussi pour n’importe quel type de blocs HTML. (ex : 10 minutes de cache sur la page, 5 minutes sur un bloc, 1 minute sur un sous-bloc ;
- Solution relativement simple et transparente pour le développeur. Ne demande pas de développement particulier, juste une réorganisation du code pour définir des actions spécifiques dans les contrôleurs.
Inconvénients :
- Le rafraichissement des blocs <esi /> dont le cache a expiré ralenti le chargement des blocs dont le cache est déjà frais. Donc dans notre cas, le gain peut être faible suivant le temps de chargement du bloc utilisant les données de session ;
- Plus le nombre de niveaux de balises esi est grand, plus la page complète mettra du temps à être générée par le serveur de cache.
Conclusion :
Les ESI sont un moyen simple de mettre en place une gestion de cache différenciée par bloc HTML.
Cependant, le gain en performance d’un point de vue utilisateur final ne sera pas forcément flagrant dans le cas de blocs non mis en cache, car ces blocs ralentissent obligatoirement le chargement du reste de la page.
Un compromis entre les deux solutions peut être plus intéressant :
- Utilisation des appels Ajax : pour les blocs utilisant des données non mises en cache ( de préférence, avec utilisation de Node.js + framework MVC JavaScript) ;
- Utilisation des ESI pour les blocs dont on veut une configuration de cache différente du reste de la page.