Bien gérer le cache navigateur avec HTTP et Node.js

Bien gérer le cache navigateur avec HTTP et Node.js

cache, nodejsPublié il y a 5 ans

Sommaire

Vous le savez certainement, mais la plupart des navigateurs modernes utilisent un cache pour éviter de télécharger deux fois une même ressource selon une stratégie bien définie.

Pour que votre navigateur puisse mettre en cache une ressource et l'exploiter correctement, votre serveur doit renvoyer quelques informations dans les entêtes de sa réponse HTTP.

  • Cache-Control : Permet de définir la stratégie à adopter pour une ressource, ainsi que la durée de vie du cache
  • ETag : L'identifiant du cache pour une ressource

Ainsi votre navigateur va savoir comment cacher, combien de temps il doit garder en cache et savoir identifier la ressource.

La directive Cache-Control peut avoir plusieurs valeurs définissant le temps de cache et la stratégie de cache, on peut distinguer les stratégies principales suivantes :

  • public : Indique que la ressource peut être mise en cache par n'importe quel acteur (navigateur, CDN, proxy etc …).
  • private : Indique que la ressource peut être mise en cache exclusivement par l'utilisateur.
  • no-cache : Indique que la ressource ne doit pas être mise en cache (page de connexion, paiement en ligne, etc …). On s'assure d'avoir du contenu frais.

Le temps de mise en cache est défini par max-age et est exprimé en seconde.

Par exemple une réponse HTTP comme telle…

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Cache-Control: public, max-age=60
Date: Thu, 29 Aug 2019 11:22:00 GMT
Connection: keep-alive

Indique que la ressource, qui est un fichier HTML, peut être mise en cache par n'importe qui pendant une durée maximale de 60 secondes. A la prochaine demande de cette ressource, le navigateur ira chercher la ressource depuis son cache.

ℹ️ Cache-Control est disponible en HTTP 1.1 et remplace les directives Expires et Pragma de HTTP 1.0.

Mais que se passe-t-il une fois les 60 secondes écoulées ? Le navigateur va tout simplement refaire une requête au serveur pour mettre à jour sa ressource et son cache. Mais si la ressource n'a pas changée même après l'expiration du cache, ce serait dommage de devoir la re-télécharger n'est-ce pas ?

C'est là que l'identifiant ETag va être utile. En effet s'il est fourni dans la réponse, le navigateur, une fois le cache expiré, va refaire un appel au serveur en ajoutant l'entête If-None-Match avec l'identifiant. Il ne restera plus qu'au serveur de comparer son identifiant et celui du navigateur pour vérifier si la ressource a changée.

Exemple de réponse HTTP du serveur avec Cache-Control et ETag :

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Cache-Control: public, max-age=60
ETag: identifiantRessource
Date: Thu, 29 Aug 2019 11:38:20 GMT
Connection: keep-alive

Ensuite une fois les 60 secondes écoulées voici la requête HTTP du navigateur :

Host: ****
User-Agent: Mozilla/5.0
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
If-None-Match: identifiantRessource

Si la ressource n'a pas changé d'identifiant, le serveur doit renvoyer une réponse 304 pour Not Modified, sans aucun corps, indiquant au navigateur que sa ressource est toujours valide et qu'il peut continuer à l'utiliser depuis son cache. Sinon le serveur renvoie la ressource entièrement.

Bien, nous avons vu la théorie, voyons maintenant la pratique. Toujours sur Node.js, mais sachez que la logique peut s'appliquer sur d'autres langages.

// index.js

const http = require('http');
const fs = require('fs');

const httpServer = http.createServer();
const timeout = 60; // Age en seconde du cache
const etag = 'monidentifiant';

httpServer.on('request', (request, response) => {

    response.setHeader('Content-Type', 'text/html');

    // On ajoute les entêtes pour le cache
    response.setHeader('Cache-Control', 'public, max-age=' + timeout);
    response.setHeader('ETag', etag);

    // On teste si l'entête If None Match est fourni
    // et qu'il correspond à l'identifiant de la ressource
    if (request.headers['if-none-match'] && request.headers['if-none-match'] === etag) {

        // On renvoie une réponse vide avec le code 304
        response.statusCode = 304;
        response.end();
    } else {

        // Sinon on renvoie le fichier html
        response.statusCode = 200;
        fs.createReadStream('index.html').pipe(response);
    }
});

httpServer.listen(8080);

Un peu d'explication : on lance un serveur HTTP qui renvoie un fichier HTML avec un cache de 60 secondes. Si la requête possède un entête If-None-Match, on compare avec notre identifiant etag pour savoir s'il faut renvoyer la ressource en 200 ou rien en 304.

Attention toutefois, dans notre code nous avons défini un identifiant unique pour toutes les ressources du serveur, idéalement il faudrait un identifiant unique pour chaque ressource. Pour ce faire, je vous conseille de hacher une propriété de votre ressource, par exemple la date de dernière modification ou la taille du fichier.

const http = require('http');
const fs = require('fs');

// Module de chiffrement
const crypto = require('crypto');

const httpServer = http.createServer();
const timeout = 60;

httpServer.on('request', (request, response) => {
    const path = 'index.html';

    // La fonction stat() permet d’obtenir des infos sur le fichier
    fs.stat(path, (err, stats) => {
        // On hache en MD5 la taille du fichier pour obtenir notre ETag
        const etag = crypto.createHash('md5')
            .update(String(stats.size))
            .digest('hex');

        response.setHeader('Content-Type', 'text/html');
        response.setHeader('Cache-Control', 'public, max-age=' + timeout);
        response.setHeader('ETag', etag);

        if (request.headers['if-none-match'] && request.headers['if-none-match'] === etag) {
            response.statusCode = 304;
            response.end();
        } else {
            response.statusCode = 200;
            fs.createReadStream(path).pipe(response);
        }
    });
});

httpServer.listen(8080);

Et tada 🎉 vous savez maintenant comment gérer correctement le cache navigateur pour vos ressources. Il existe d'autres valeurs pour Cache-Control disponibles sur le site MDN ainsi qu'une documentation plus générale sur le cache sur le même site ici.

Enfin je vous propose, pour compléter cette article, un billet très complet de Mark Nottingham : Un tutoriel de la mise en cache.

Commentaire(s) 

Ancun commentaire pour le moment.