Objectif

La mise en place de caches permet d’améliorer les performances de votre application et ainsi son confort d’utilisation.
Elle permet également de réduire les ressources nécessaires à son fonctionnement: bande passante, temps machine.

Cette page décrit les bonnes pratiques permettant de mettre en place des caches conformément aux standards du web.

Cache client

Entêtes Cache-Control et ETag

Deux mécanismes permettent de limiter les requêtes client/serveur.

Cache-Control

Principe

La mise en place de l’entête Cache-Control permet d’indiquer au navigateur une période durant laquelle le résultat de la requête est valide. Ainsi, la requête ne sera pas relancée.

Ceci est particulièrement adapté aux éléments ne changeant que lentement (référentiels, liste de campagnes ou de sites, images…).  

Mise en place

Côté serveur, le Cache-Control concerne les requêtes de type GET. La décision d’utiliser ce mécanisme doit se faire entité par entité chacune ayant potentiellement une durée de vie différente. 

Exemple de mise en oeuvre: 

response.setHeader("Cache-Control", "max-age=300, private");

 Côté client, le fait que la requête soit exécutée réellement ou utilise le cache est transparent. Toutefois, il faut veiller à mettre en place un indicateur graphique de chargement pour les fois où le requête est réellement exécutée. 

ETag

Principe

Le ETag repose également sur un mécanisme d’entête HTTP. Il s’utilise de manière complémentaire au Cache-Control. L’idée est d’assigner une chaîne de texte à une entité retournée par une requête: le ETag. Il peut s’agir d’un md5, de la date de dernière modification… Si le navigateur possède encore en cache l’entité et son ETag après l’expiration du Cache-Control, il va l’envoyer via l’entête If-None-Match. Si le serveur estime que ce ETag correspond toujours à l’entité, il va renvoyer un code HTTP 302 (Not Modified). Dans le cas contraire il va renvoyer l’entité normalement (accompagnée de son nouveau ETag).

Ainsi le Etag permet, si l’entité n’a pas été modifiée, d’économiser 

  • Du temps de calcul côté serveur si on peut se dispenser de recalculer l’entité
  • De la bande passante pour la réponse (seul un code 302 est renvoyé)
  • Du temps pour l’utilisateur de l’application. 
Mise en place

En Spring Boot, il est très facile de mettre en place un filtre implémentant ce principe. Ce filtre générique a un défaut important, il calcule le ETag après avoir tout de même généré la réponse. Il permet donc de gagner essentiellement de la bande passante. Si l’entité est rapide à calculer, ce mécanisme par défaut reste pertinent 

Mise en oeuvre d’un ShallowEtagHeaderFilter: 

@Bean
public Filter etagFilter() {
		return new ShallowEtagHeaderFilter();
	}

Par contre, quand les entités pour lesquels calculer l’ETag sans avoir à générer la réponse est possible, le traitement peut être mis dans un filtre ou dans le traitement de la requête.

Mise en oeuvre d’un ETag dans une requête GET: 

@GetMapping("request")
    public ResponseEntity<S2MAreaProductByYearsDownloadFilter> getRequestResponse(@RequestParam("collection") String metadataIdentifier, HttpServletRequest httpServletRequest) {
        HttpHeaders headers = new HttpHeaders();
        String ifNoneMatchHeader = httpServletRequest.getHeader("If-None-Match");
        HttpStatus httpStatus = HttpStatus.OK;
        S2MAreaProductByYearsDownloadFilter s2MAreaProductByYearsDownloadFilter = null;
        String etag = null;
        Optional<MetadataUpdateState> metadataUpdateState = metadataUpdateStateRepository.findById(metadataIdentifier);
        if (metadataUpdateState.isPresent()) {
            etag = String.valueOf(LocalDateTime.parse(metadataUpdateState.get().getLastUpdateDate()).atZone(ZoneId.of("Europe/Paris")).toEpochSecond());
        }
        if (ifNoneMatchHeader == null || (ifNoneMatchHeader != null &amp;&amp; ifNoneMatchHeader != etag)) {
            s2MAreaProductByYearsDownloadFilter = s2MDataTools.getS2MAreaProductByYearsDownloadFilter(fileInPath);
        } else {
            httpStatus = HttpStatus.NOT_MODIFIED;
        }

        headers.set("Cache-Control", "max-age=600, private");
        headers.setETag("W/\"" + etag + "\"");
        return new ResponseEntity<>(s2MAreaProductByYearsDownloadFilter, headers, httpStatus);
    }

Utilisation du LocalStorage et du Session Storage

Le LocalStorage et le SessionStorage sont deux fonctionnalités des navigateurs modernes permettant de stocker sur le poste du client des informations volumineuses sous la forme clé/valeur.

La différence entre LocalStorage et SessionStorage réside dans le fait que les informations du SessionStorage sont vidées à la fermeture du navigateur. Les informations du LocalStorage sont eux pérennes.

Avec l’utilisation correcte des entêtes Cache et ETag, l’utilisation du LocalStorage ou du SessionStorage devrait se limiter aux informations propres à l’état du client.

Ceci correspond, d’un point de vue vue.js: le store (au sens vuex). Cela permet ainsi de permettre à un utilisateur de pouvoir rétablir l’état de l’application en faisant un rafraîchissement de la page ou en retour d’authentification voire en cas de réouverture du navigateur (en cas d’utilisation de LocalStorage).  Dans vue.La synchronisation store/storage est particulièrement simple à mettre en oeuvre avec vuex-persist.

 Par contre, aucun résultat de requête ne devrait être conservées dans les storages. Seul les mécanismes de cache doivent être utilisés.

Cache serveur

Cache Mémoire

//A détailler

Autres briques

D’autres mécanismes de cache pourrait être également mis en place côté serveur comme un serveur Redis dédié. Cela sera étudié ultérieurement.

Crédits Photo

unsplash-logoMichael Dziedzic