Tutoriel Vidéo PHP Gestion du cache Etag et Last-Modified

Télécharger la vidéo

Aujourd'hui il existe de nombreaux moyens d'améliorer les performances d'une application. Le cache HTTP est souvent une méthode ignorée bien que très efficace qui est utilisé par les navigateurs mais aussi par des proxy intermédiaires comme Varnish.

Le cache HTTP est une méthode universelle qui dispose de spécifications précises adoptées par tous les navigateur modernes. L'implémentation de ces standarts peut permettre d'améliorer grandement les temps de réponses et l'expérience des utilisateurs.

Principe de base

Lorsque le navigateur télécharge une ressource disposant des bonnes en-tête il va sauvegarder une copie local du contenu pour un accès plus rapide les prochaines fois que la ressource est demandée.

Headers

Il y a 2 headers principaux liés au cache : Cache-Control et Expires.

Cache-Control

Le header Cache-Control est le header le plus important car il permet d'activer la mise en cache d'une page. Sans ce header le navigateur redemandera systématiquement la ressource au serveur.

Cache-Control:public # Active le cache pour le navigateur ou le proxy intermédiaire (varnish par exemple)
Cache-Control:private # Active le cache pour le navigateur mais pas pour le proxy intermédiaire
Cache-Control:public, max-age=600 # Précise la durée de mise en cache (en seconde)

Plus d'informations sur les valeurs possible du Cache-Control

Expires

En plus du Cache-Control, le header Expires permet de définir une date à partir de laquelle la ressource cachée ne sera plus valide. Au delà de cette date, la ressource sera redemandée au serveur et remis en cache.
Attention, si max-age et Expires sont définis alors max-age prendra le dessus.

Cache-Control:public
Expires: Sat, 14 Mar 2048 14:49:43 GMT

Last-Modified

Le header Last-Modified permet de s'assurer que la ressource n'a pas changée depuis la précédente mise en cache. Pour connaitre la validité de la version en cache le navigateur fera une requête au serveur en lui envoyer la valeur de Last-Modified correspondant à la version mise en cache. Si la ressource n'a pas été modifiée depuis la date indiqué le serveur renverra alors un code de réponse 304 (Not Modified).

Cache-Control:public, max-age=604800
Last-Modified: Wed, 06 Jul 2016 13:01:52 GMT

Etag (Entity Tag)

L'Etag fonctionne d'une manière similaire mais sa valeur n'est pas une date mais une clef représentant le contenu.

Cache-Control:public, max-age=604800
ETag: "0758337d8e41bc40170e45bf7c4a51fc"

Cette méthode est plus couteuse côté serveur car il faut générer la réponse pour ensuite généré un hash (il ne permet au final que d'économiser le transfert de données). Mais il est possible d'utiliser un Etag "faible" qui permet d'identifier 2 pages "semblables".

Cache-Control:public, max-age=604800
ETag: W/"0758337d8e41bc40170e45bf7c4a51fc"

Cet Etag peut être obtenu à partir de la date de dernière modification par exemple ou une autre information permettant d'identifier une même version de la page.

Implémentation PHP

Maintenant que l'on a compris les concepts de base on va rapidement passer sur l'implémentation PHP des 2 headers Etag et Last-Modified. Il n'est pas possible d'utiliser la méthode Expires pour une page générée en PHP car on ne connait pas à l'avance la durée de validité de la page en question.

<?php
// On utilise ici la date de modification du fichier 
// comme référence mais on peut utiliser une autre donnée
$file = __FILE__;
$last_modified_time = filemtime($file);

// On génère l'ETAG faible
$etag = 'W/"' . md5($last_modified_time) . '"';

// On définit les bons headers
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $last_modified_time) . " GMT");
header('Cache-Control: public, max-age=604800'); // On peut ici changer la durée de validité du cache
header("Etag: $etag");

// On vérifie les headers envoyés dans la requête
if (
(isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) === $last_modified_time) ||
(isset($_SERVER['HTTP_IF_NONE_MATCH']) && $etag === trim($_SERVER['HTTP_IF_NONE_MATCH']))
) {
    // On renvoit une 304 si on n'a rien modifié depuis
    header('HTTP/1.1 304 Not Modified');
    exit();
}

// Le reste de notre script se déroulera ici