Optimiser l’expérience utilisateur avec le cache HTTP et le Rendu Segmenté

Eric Burel
PointJS
Published in
10 min readMar 6, 2024

--

Fusionnez un million de rendus HTML d’une page web en seulement deux grâce au cache HTTP et au Rendu Segmenté

Cet article est long : il est encore meilleur avec un thé ou un café ! Il cible les développeurs web de niveau intermédiaire, avec une connaissance basique du rendu côté serveur et du rendu statique. Si vous débutez, vous pouvez commencer par ma playlist sur le tutoriel Next.js Learn, en français.

De pauvres clients et de riches visiteurs

Imaginez vous en train de lire votre magazine en ligne favori. Pour moi, ce serait n’importe quel blog décrivant des synthétiseurs obscurs et très chers que je n’achèterai jamais, et que je ne saurais pas utiliser de toute façon.

Les performances du site sont merveilleuses. Comme le contenu est public et qu’il est identique pour tous les utilisateurs, les développeurs ont fait en sorte qu’il soit mis en cache près de chez vous, “at the edge”, et envoyé directement sous forme d’HTML, grâce au rendu statique.

Imaginez que ce site ait une section payante, pour les articles vraiment excellents. Ce sera toujours moins cher qu’un bon synthé analogique ! Vous payez votre abonnement, vous vous connectez, et tout d’un coup, tout devient plus lent.

Des “spinners” de chargement apparaissent partout. Votre processeur devient bruyant. Vous attendez trois secondes pour que chaque page soit chargée. Vous pourriez faire frire un œuf sur votre Ipad brûlant.

Que se passe-t-il ? Vous venez de quitter le monde merveilleux du rendu statique. Maintenant, tout devient rendu côté client en JavaScript ou alors rendu côté serveur pour chaque nouvelle requête.

J’appelle cela le problème du “pauvre client et du riche visiteur”. Les visiteurs profitent de performances optimales, avec du rendu statique ou du moins de la mise en cache, tandis que les utilisateurs connectés ont les moins bonnes performances, avec du rendu à la requête ou dans le navigateur. Ce n’est pas très cohérent !

Essayons de résoudre ce problème.

La musique s’arrête brutalement, arrêt sur image : voilà, c’est moi, et vous vous demandez sûrement comment je me suis retrouvé avec un score de 38% de performance sur Google Lighthouse.

1 segment devrait donner 1 rendu HTML

L’analyse marketing divise les gens entre segments homogènes. Les visiteurs non connectés sur un site forment un segment, et les clients abonnés constituent un autre segment.

Seul le segment des abonnés peut accéder au contenu payant, tandis que les visiteurs doivent être redirigés vers une page d’abonnement.

Cette notion intervient dans beaucoup d’autres situations qui impliquent de montrer un contenu différent selon le segment :

  • les tests A/B : tous les utilisateurs d’un groupe (“bucket”) verrons la même version du site, A ou B
  • la multi-tenancy : tous les utilisateurs d’une entreprise donnée verrons le même logo et le même thème
  • le contenu traduit : tous les utilisateurs parlant un même langage voient le même texte
  • thème sombre ou clair : tous les utilisateurs ayant choisi un thème clair détruisent leur cornée ainsi que l’environnement

Dans ces exemples, tous les utilisateurs d’un segment devraient donc voir le même contenu. Il serait pertinent de mettre en place un cache, ou une forme de rendu statique, pour ne réaliser qu’un seul rendu HTML par segment. Comme ça, nos clients abonnés profiteraient aussi des meilleures performances possibles.

C’est ce que j’appelle le “Rendu Segmenté”.

Implémenter le Rendu Segmenté

Notre objectif est donc de réaliser un seul rendu par segment. Cela va réduire le coût d’opération de notre site, puisque pour répondre à une requête il suffit de renvoyer du contenu HTML depuis un cache, sans avoir à appeler la base de données ou réaliser un calcul. Cela va aussi réduire les temps de chargement pour l’utilisateur final, qu’il s’agisse d’un visiteur ou d’un abonné.

Malheureusement, la plupart des frameworks “jamstack” font l’erreur de supposer que si un contenu n’est pas public et identique pour tout le monde, il ne peut pas bénéficier du rendu statique. Cette idée fausse rend difficile l’implémentation du rendu segmenté.

Découvrons deux moyens de dépasser cette limitation.

Une implémentation spécifique à Next.js

J’ai décrit dans un article précédent comment implémenter le rendu segmenté avec Next.js et le Megaparam. J’ai aussi prouvé mathématiquement que cette architecture permet d’atteindre le minimum possible de rendus, poussant le rendu statique à l’extrême. Cela m’a permis d’ouvrir quelques pull requests pour améliorer les documentations de différents frameworks, notamment Next.js, Qwik et Astro.

La limite de cette approche est qu’elle s’appuie sur les fonctionnalités spécifiques de Next.js, comme les middlewares et les méthodes “generateStaticParams” (anciennement “getStaticPaths”/”getStaticProps”).

Dans un framework tel que Remix, l’équivalent serait de mettre en place un cache au niveau applicatif, dans le fichier “entry.server”.

On peut généraliser le raisonnement à tous les frameworks jamstack en ajoutant un serveur de redirection juste avant l’application principale. Ce serveur détecte le segment auquel l’utilisateur appartient, puis redirige l’utilisateurs vers la bonne variante de la page rendue en statique (une réécriture d’URL permet une redirection invisible).

Une architecture pour généraliser le rendu statique, que j’appelle “rendu segmenté” ou “rendu arc-en-ciel”.
Résultat du rendu Segmenté avec Next.js, la démo est par ici : https://megaparam-demo.vercel.app/

Mais cela reste plus complexe que nécessaire, car il faut un cache au niveau de l’application. Essayons de faire mieux en utilisant uniquement une technologie standard et disponible partout : le cache HTTP.

Une implémentation standard avec le cache HTTP

J’ai tendance à utiliser les termes “statique” et “mis en cache” de façon interchangeable. C’est parce que le rendu statique est équivalent à une mise en cache du rendu HTML (à partir d’un template React, PHP, etc.) au moment de la compilation de l’application.

Donc, mettre un cache configuré en amont de n’importe quel framework de rendu serveur peut transformer le rendu dynamique pour chaque requête en rendu statique.

C’est pourquoi le cache HTTP est un candidat intéressant pour implémenter le rendu segmenté.

Cette philosophie visant à “privilégier l’utilisation des fondements du navigateur web” est typique du framework Remix. Le protocole HTTP est un standard qui intègre déjà un mécanisme de cache, et qui est entièrement documenté sur MDN Web Docs.

Essayons donc d’utiliser ce cache, en particulier un cache de type “partagé” entre utilisateurs, pour implémenter le rendu segmenté.

Cinq étapes sont nécessaires, accrochez-vous 🎢!

1. Mettre en place le rendu serveur à la requête

Pour faire du rendu segmenté, il faut pouvoir générer un contenu HTML pour chaque variante de la page, par exemple une version par langage parlé par les utilisateurs.

Il faut donc que notre application dispose d’une capacité de rendu serveur, et ce rendu doit pouvoir être déclenché par une requête HTTP, puisque nous utiliserons ensuite le cache HTTP.

Dans Next.js, on utilisait précédemment la méthode “getServerSideProps”. Désormais, on utiliserait plutôt l’utilitaire “noStore()” appelé au sein d’un React Server Component au niveau de la page.

// Un composant dynamique dans Next.js
export async function MyBlogPost(params) {
noStore() // cela rend la page dynamique avec un rendu par requête HTTP
const content = await getPost(params.postId)
return <div>{content}</div>
}

2. Identifier le segment au moment de la requête HTTP

Etant donné la requête de l’utilisateur, il faut pouvoir en déduire son segment. Rien à coder dans cette étape, il s’agit simplement de bien comprendre le lien entre les segments que vous avez défini, et chaque requête HTTP.

Exemple: le langage supposé de l’utilisateur est accessible via l’en-tête Accept-Language de la requête HTTP, on va donc l’utiliser comme clé de cache.

3. Mettre en place l’en-tête “Vary” de la réponse HTTP

Il s’agit maintenant de configurer le cache HTTP à proprement parler, pour qu’il stocke une variante par segment, et qu’il puisse la retrouver ensuite.

La première fois qu’un utilisateur français visite le site, on va donc générer la page en français, et dire au cache HTTP partagé de la garder en mémoire. Le second utilisateur français recevra directement la version mise en cache.

Il faut donc lister les éléments de la requête pertinents dans l’en-tête “Vary”, documentée ici.

Exemple: ajoutez cette en-tête à la réponse HTTP Vary: Cookie, Accept-Language

4. Définir la durée de rétention

L’en-tête “Vary” indique quels paramètres de la requête changent entre les segments, mais il faut aussi signaler qu’on veut stocker la page dans un cache partagé public, pour une durée donnée.

Cela se fait via l’en-tête Cache-Control. La durée “maxage” est donnée en seconde, et il faut faire attention à bien calibrer cette durée. Le problème est que vous n’avez pas un contrôle direct sur le cache HTTP, qui est implémenté par votre hébergeur : difficile de le vider en cas de problème !

Exemple: Cache-Control: public, s-maxage=604800

5. Et voilà!

En utilisant ces en-têtes HTTP, vous avez mis en place le rendu segmenté, il n’y aura plus qu’un rendu par segment utilisateur !

Il ne s’agit pas d’un rendu “statique” au sens classique, car le rendu n’est pas généré à la compilation mais plutôt lors de la première requête. On parle parfois de rendu incrémental. Mais c’est un détail, car le nombre de rendu est identique à celui obtenu via une approche statique : un seul par segment. Il s’agit donc bien d’une forme généralisée de rendu statique.

On peut donc personnaliser le contenu, comme on le ferait avec du rendu dynamique au moment de la requête, mais avec les performances du rendu statique !

Bonus: ETag pour versionner les variations

L’en-tête “ETag” permet d’assigner une version au contenu renvoyé dans la réponse. Ici, l’en-tête ETag ne devrait contenir qu’un identifiant unique par segment, associé à un identifiant de version par exemple le dernier commit de votre dépôt Git.

On peut notamment l’utiliser côté navigateur dans le cas où l’utilisateur change beaucoup de segment (essai de plusieurs thèmes par exemple), pour limiter le nombre de requêtes.

Exemple : ETag: bucket-b-theme-light-commit-1234

Limitations du cache HTTP pour le rendu segmenté

Nous avons défini deux façons d’implémenter le rendu segmenté, une spécifique à Next.js, l’autre universelle à partir du cache HTTP public.

Cependant, ces approches ne sont pas vraiment équivalentes. La version spécifique à Next.js s’avère beaucoup plus flexible.

Voici les limites de la version utilisant le cache HTTP.

1. Dépendance au rendu dynamique

Comme on l’a dit précédemment, le cache HTTP nécessite une requête HTTP pour être activé, on ne peut donc pas le remplir au moment de la compilation, mais seulement après le déploiement du site. Cela signifie aussi qu’il nous faut un framework de rendu serveur dynamique, par exemple une instance de Next.js ou de Remix. Ce n’est pas un point bloquant pour la majorité des projets, mais il faut en être conscient.

2. Dépendance au CDN

Le cache HTTP est géré par l’hébergeur, au niveau infrastructure, typiquement via un Content Delivery Network (CDN).

  • L’en-tête Vary, qui nous sert à définir l’élément de la requête qui distingue les segments (par exemple l’en-tête Accept-Language pour la langue), est rarement supportée. Elle n’était pas supporté par Vercel en 2022 quand j’ai écrit la première version de cet article en anglais et elle ne l’est toujours pas en 2024.
  • Le CDN peut ajouter des règles de sécurité et de protection de la vie privée. Typiquement, les requêtes contenant une en-tête “Authorization” ne sont pas mises en cache sur Vercel (même si cela ne limite en pratique que l’authentification basique, et non l’authentification via un cookie).
  • Même lorsqu’elle est supportée, l’en-tête Vary est peu précise, on ne peut par exemple pas choisir quel cookie prendre en compte. Cet article propose une en-tête “Key” pour résoudre ce problème, mais elle n’existe pas en pratique à ma connaissance.

3. Absence de gestion du contenu privé et payant

Le cache HTTP peut être soit:

  • partagé, le rendu est mis en cache sur le CDN qui est public
  • privé, le rendu est mis en cache côté client, donc dans le navigateur

Il n’est donc pas vraiment possible de mettre en cache un rendu privé sur un CDN, il n’y a pas de garantie qu’il ne sera pas accessible à d’autres utilisateurs, et il n’est généralement pas non plus possible d’ajouter une logique de vérification de l’identité de l’utilisateur ou de s’il bénéficie d’un abonnement payant.

Pour revenir à notre cas d’usage initial, le rendu segmenté avec le cache HTTP ne fonctionnera malheureusement pas pour notre blog payant. Il ne résout pas pleinement le problème des “pauvres clients et des riches visiteurs”.

La seule option valable est d’implémenter un cache côté serveur au niveau applicatif, ce que propose un framework comme Next.js. Cela nous donne un contrôle accru sur les permissions d’accès et la clé utilisée pour la mise en cache du rendu.

Conclusion : un cache au niveau applicatif reste plus efficace que le cache HTTP

En résumé, sous certaines conditions, le cache HTTP permet d’implémenter une forme de rendu statique, mais personnalisé, que j’appelle le rendu segmenté. On peut donc avoir plusieurs variations d’une même page, tout en minimisant les calculs.

Les cas d’usage sont l’internationalisation du contenu, les tests A/B, les thèmes, la multi-tenancy etc. Cela marche en particulier très bien si le segment est contenu dans l’URL (“/fr” pour français) ou une en-tête HTTP particulière (“Accept-Language”).

Mais si vous voulez aller plus loin, la seule solution viable est de mettre en place un serveur de redirection en amont de l’application pour pointer l’utilisateur vers la bonne variante, combiné avec un framework supportant le rendu statique au niveau applicatif, comme Next.js.

J’explique comment faire cela dans un article précédent. Vous pouvez aussi implémenter un système maison vous-mêmes, cela marche très bien !

Vous avez maintenant toutes les cartes en main pour rendre vos clients riches, et vos visiteurs, exactement aussi riches que vos clients !

Quelques ressources pour approfondir

Découvrez ma formation Next.js en 3 jours

Mes masterclass en 1 journée sur des sujets spécifiques

Une playlist Youtube où l’on suit le tutoriel Next.js Learn, en français

Et mes cours vidéos (en anglais)

Découvrez ma formation vidéo “Next.js Patterns”: https://course.nextjspatterns.com/manage-client-only-code-in-next-js-with-react-browser-components
https://nextjspatterns.com/

--

--

Eric Burel
PointJS

Next.js teacher and course writer. Co-maintainer of the State of JavaScript survey. Follow me to learn new Next.js tricks ✨ - https://nextjspatterns.com/