Rendez vos applications React et Next.js plus rapides que la lumière grâce à React ESI

Retrouvez la version originale de cet article ici.

React ESI est une bibliothèque de cache extrêmement puissante pour les applications React et Next.js, capable de créer des applications dynamiques aussi rapides que des sites statiques.

React ESI offre un moyen simple d'améliorer les performances de votre application en stockant des fragments de pages rendues côté serveur dans des serveurs de cache périphériques (edge cache servers) tels que ceux proposés par Fastly ou Akamai (le logiciel Varnish est également supporté). Cela signifie qu'après le premier rendu, des fragments de vos pages seront servis en quelques millisecondes par des serveurs proches de vos utilisateurs finaux ! React ESI est un moyen très efficace d'améliorer les performances et le référencement de vos sites web, ainsi que pour réduire considérablement vos coûts d'hébergement et la consommation d'énergie de ces applications.

Vu qu'il repose sur la spécification W3C ESI (Edge Side Includes), React ESI prend en charge de manière native la plupart des fournisseurs de cache cloud comme Cloudflare Workers, Akamai et Fastly. Bien entendu, React ESI supporte également le serveur de cache open source Varnish, que vous pouvez utiliser gratuitement dans votre propre infrastructure (voici un exemple de configuration).

React ESI permet de spécifier un TTL (Time To Live) différent par composant React, puis de générer le code HTML lui correspondant de manière asynchrone à l'aide d'une URL sécurisée (signée). Le serveur de cache récupère ce fragment de HTML depuis l’application Node puis le stocke. Il assemble finalement ensuite la page finale et l'envoie au navigateur. React ESI permet également aux composants d’être rendus par le navigateur côté client, sans nécessiter de configuration spécifique.

react esi


Exemple : découvrez comment utiliser React ESI avec Next.js et Varnish

Installation

Utilisez votre gestionnaire de paquets JS préféré pour installer la bibliothèque.

Avec Yarn :

$ yarn add react-esi

Ou avec NPM :

$ npm install react-esi

Usage

React ESI fournit un Higher Order Component qui :

  • Remplace le composant encapsulé par une balise ESI côté serveur (ne vous inquiétez pas, React ESI fournit également l'outillage nécessaire pour générer le fragment correspondant) ;
  • Refait le rendu du composant encapsulé côté client et l’alimente avec les props calculées côté serveur (si nécessaire).

React ESI appelle automatiquement une méthode static async nommée getInitialProps() pour renseigner les props initiales du composant. Côté serveur, cette méthode peut accéder à la requête et à la réponse HTTP pour, par exemple, définir l'en-tête Cache-Control ou des cache tags.

Ces props renvoyées par getInitialProps() seront également injectées dans le code HTML généré par le serveur (dans une balise <script>). Côté client, le composant réutilisera les props provenant du serveur (la méthode ne sera pas appelée une seconde fois). Si la méthode n'a pas été appelée côté serveur, elle sera appelée côté client lors du premier montage du composant.

Le Higher Order Component

// pages/index.js
import React from 'react';
import withESI from 'react-esi';
import MyFragment from 'components/MyFragment';

const MyFragmentESI = withESI(MyFragment, 'MyFragment');
// Le second paramètre est un ID unique identifiant ce fragment.
// Si vous utilisez différentes instances du même composant, utilisez un ID différent par instance.

const Index = () => (
  <div>
    <h1>React ESI demo app</h1>
    <MyFragmentESI greeting="Hello!" />
  </div>
);
// components/MyFragment.js
import React from 'react';

export default class MyFragment extends React.Component {
  render() {
    return (
      <section>
        <h1>A fragment that can have its own TTL</h1>

        <div>{this.props.greeting /* accédez aux props comme d’habitude */}</div>
        <div>{this.props.dataFromAnAPI}</div>
      </section>
    );
  }

  static async getInitialProps({ props, req, res }) {
    return new Promise(resolve => {
      if (res) {
        // Réglage d’un TTL pour le fragment
        res.set('Cache-Control', 's-maxage=60, max-age=30');
      }

      // Simulation d’un délai (appel à un service distant tel qu’une API web)
      setTimeout(
        () =>
          resolve({
            ...props, // Ce sont les props venant de index.js, qui ont été transmises via l’URL interne
            dataFromAnAPI: 'Hello there'
          }),
        2000
      );
    });
  }
}

Les props initiales doivent être sérialisables avec JSON.stringify(). Attention à Map, Set et Symbol !

Nb : pour plus de commodité, getInitialProps() a la même signature que la fonction du même nom fournie par Next.js. Cependant, c'est une implémentation totalement indépendante et autonome (vous n'avez pas besoin de Next.js pour l'utiliser).

Servir les fragments

React ESI fournit un contrôleur prêt à l’emploi compatible avec Express :

// server.js
import express from 'express';
import { path, serveFragment } from 'react-esi/lib/server';

const server = express();
server.use((req, res, next) => {
  // Envoie l’en-tête Surrogate-Control pour annoncer aux proxys le support des ESI (optionnel avec Varnish, suivant votre config)
  res.set('Surrogate-Control', 'content="ESI/1.0"');
  next();
});

server.get(path, (req, res) =>
  // "path" a comme valeur par défaut /_fragment, vous pouvez la changer en réglant la variable d'environnement REACT_ESI_PATH
  serveFragment(
    req,
    res,
    // "fragmentID" est le second paramètre passé au HOC "WithESI", le composant racine utilisé pour ce fragment doit être retourné
    fragmentID => require(`./components/${fragmentID}`).default) 
);

// ...
// Les autres routes  Express sont ici

server.listen(80);

Alternativement, voici un exemple complet utilisant un serveur Next.js :

// server.js
import express from 'express';
import next from 'next';
import { path, serveFragment } from 'react-esi/lib/server';

const port = parseInt(process.env.PORT, 10) || 3000
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare().then(() => {
  const server = express();
  server.use((req, res, next) => {
    // Envoie l’en-tête Surrogate-Control pour annoncer aux proxys le support des ESI (optionnel avec Varnish)
    res.set('Surrogate-Control', 'content="ESI/1.0"');
    next();
  });

  server.get(path, (req, res) =>
    serveFragment(req, res, fragmentID => require(`./components/${fragmentID}`).default)
  );
  server.get('*', handle); // Routes Next.js

  server.listen(port, err => {
    if (err) throw err;
    console.log(`> Ready on http://localhost:${port}`);
  });
});

Fonctionnalités

  • Support de Varnish, Cloudflare Workers, Akamai, Fastly et tous les autres systèmes de cache prenant en charge ESI
  • Écrit en Typescript
  • API similaire à Next.js

Variables d’environnement

React ESI peut être configuré à l'aide de variables d'environnement :

  • REACT_ESI_SECRET : une clé secrète utilisée pour signer l'URL du fragment (il est vivement recommandé de la définir pour éviter les problèmes lors du redémarrage du serveur ou de l'utilisation de multiples serveurs)
  • REACT_ESI_PATH : le chemin interne utilisé pour générer le fragment, qui ne doit pas être exposé publiquement (par défaut : /_fragment)

Passage d'attributs à l'élément <esi:include>

Pour transmettre des attributs à l'élément <esi:include> généré par React ESI, passez une prop au HOC ayant la structure suivante :

{
  esi: {
    attrs: {
      alt: "Texte alternatif",
      onerror: "continue"
    }
  }
}

Dépannage

  • Le cache n’est jamais utilisé  

Par défaut, la plupart des proxys (y compris Varnish) ne servent jamais de réponse à partir du cache si la demande contient un cookie. Si vous testez avec localhost ou un domaine local similaire, effacez tous les cookies préexistants pour cette origine. Si les cookies sont attendus (par exemple : Google Analytics ou des cookies publicitaires), vous devez configurer correctement votre proxy de cache pour les ignorer. Voici quelques exemples pour Varnish

Aller plus loin

React ESI fonctionne très bien avec les stratégies de cache avancées, notamment :

  • l'invalidation du cache (purge) avec les tags de cache (Varnish / Cloudflare)
  • le préchauffage du cache lorsque les données changent dans la couche de persistance (Varnish)

Essayez-les !

Vue.JS/Nuxt

Nous aimons tout autant Vue et Nuxt que React et Next. Nous portons donc actuellement React ESI vers cette plate-forme. Contactez-nous si vous voulez aider !

Passez le mot !

Vous aimez React ESI ? Rejoignez les 200 personnes lui ayant déjà mis une étoile sur Github et n’hésitez pas à contribuer ! Notre coopérative sera également ravie de vous accompagner dans la mise en place de React ESI dans votre projet :)