Comment gérer simplement la mise en cache navigateur ?

MaJ 17/03/2011 : pour ceux qui ont déjà lu l’article, j’ai déplacé le numéro de version (avant, mis comme un répertoire) dans le nom du fichier. C’est plus propre pour les URLs relatives des fichiers CSS.

Les fichiers ressources, dits statiques, sont des fichiers dont le contenu ne change pratiquement jamais. C’est le cas des fichiers CSS, JavaScript, images, etc.

Donc, il peut s’avérer utile, voir indispensable, de les stocker en cache. Cela permet à la fois :

  • de préserver la bande passante.
  • et de limiter les requêtes inutiles. Ce qui accélère l’affichage des pages et surtout libère le serveur. Ce dernier peut s’occuper de chose plus importante.

Le fonctionnement courant est l’échange d’entêtes Last-Modified/If-Modified-Since ou encore ETag/If-None-Match. Mais nous verrons une autre solution que j’utilise.

Last-Modified/If-Modified-Since

Ces entêtes permettent l’échange d’une information datée. Si le navigateur a déjà une ressource en cache, il envoie dans ses entêtes une information If-Modified-Since demandant au serveur de lui envoyer le contenu seulement si celui-ci a changé depuis une date donnée (date de dernière modification en général envoyée par le serveur avec Last-Modified). Si la ressource n’a pas été modifiée, alors le serveur envoie le code 304 NOT MODIFIED.

ETag/If-None-Match

Ces entêtes utilisent une information unique basée sur un hash généré par le serveur. Lorsque le navigateur télécharge une ressource, il l’a place en cache et garde en mémoire son ETag (Entity Tag). Lorsque le navigateur redemande la ressource, il place dans son entête une information If-None-Match correspondant au hash. Le serveur compare ce dernier à celui de la ressource. Si le hash est identique, le code 304 NOT MODIFIED est envoyé.

Le coté pratique est que le code 304 NOT MODIFIED indique au navigateur qu’il est inutile de télécharger à nouveau le contenu de la ressource. Le gain en bande passante est alors garanti.

Exemple d’entête HTTP

Requête :
GET ************* HTTP/1.1
Host: ************
User-Agent: Mozilla/5.0 (X11; Linux i686; rv:2.0b7) Gecko/20100101 Firefox/4.0b7
Accept: image/png,image/*;q=0.8,*/*;q=0.5
Accept-Language: fr-fr
Accept-Encoding: gzip, deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 115
Connection: keep-alive
Referer: ******************
If-None-Match: 1187711318n

Réponse:
HTTP/1.1 304 NOT MODIFIED
Server: *****
Date: Sun, 28 Nov 2010 13:08:37 GMT
Content-Type: image/png
Connection: keep-alive
Etag: 1187711318n
Pragma: no-cache
Cache-Control: no-cache

Nous voyons ici que Etag est utilisé.

Problématique

Avec ces entêtes, nous ne pouvons pas garantir que le fichier en cache sera bien mis à jour si la ressource change. Après une mise à jour d’un site web, il est courant de voir les fichiers CSS ou encore les images rester dans leur ancienne version. La résultat peut être catastrophique (graphiquement parlant).

Nous pourrions changer le nom des fichiers, mais cela peut représenter une sacré charge de boulot.

Ma solution

Effectivement, j’écris bien ma solution, car il en existe plusieurs, il en va de soit. Ma technique consiste à modifier dynamiquement le chemin vers les ressources et de configurer en plus de cela l’expiration de ces ressources. Le système est basé sur une constante PHP et le mod_rewrite.

La constante contient une sorte de numéro de version que je place dans le chemin de la ressource.

<?php
define('APPLICATION_REV', '1.0');
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr" lang="fr">
    <head>
        <title>Page de test</title>
        <meta http-equiv="content-type" content="text/html; charset=utf-8" />
        <link href="/css/<?php echo APPLICATION_REV; ?>-styles.css" type="text/css" rel="stylesheet" />
    </head>
    <body>It's work</body>
</html>

Voici pour la première étape.
Maintenant, nous mettons en place le fichier .htaccess (j’utilise Apache, adaptez selon votre environnement).

<IfModule mod_expires.c>
    ExpiresActive on
    ExpiresByType application/x-javascript "access plus 1 month"
    ExpiresByType application/javascript "access plus 1 month"
    ExpiresByType text/javascript "access plus 1 month"
    ExpiresByType text/css "access plus 1 month"

    ExpiresByType image/* "access plus 1 month"
</IfModule>

<IfModule mod_rewrite.c>
    RewriteRule ^css/(.*)-[.0-9]+\.css$ /css/$1.css [L]
    RewriteRule ^js/(.*)-[.0-9]+\.js$ /js/$1.js [L]
    RewriteRule ^images/(.*)-[.0-9]+\.([^.])$ ./images/$1.$2 [L]
</IfModule>

En première partie, je place la configuration de l’expiration des ressources. Je me contente ici des fichiers CSS, JavaScript et des images, mais vous pouvez l’étendre à d’autres types de ressource (ex: Flash). Vous voyez que je configure l’expiration à 1 mois. La ressource ne sera donc jamais (en théorie) téléchargée une nouvelle fois, même si elle change sur le serveur. Le navigateur ne fera donc aucune requête de vérification.

Dans la seconde partie, je réécris les URLs pour prendre en compte le paramètre dynamique (la constante PHP).

Exemple : /css/styles-1.0.css est réécrit en /css/styles.css.

Le fichier CSS sera donc téléchargé est mis en cache pour un mois. À la prochaine mise à jour du site, il suffira de modifier la valeur de la constante (ex: 1.5), et le fichier sera téléchargé à nouveau puisque le navigateur ne trouvera pas de correspondance dans son cache.

Conclusion

Vous voyez donc que ma solution est très simple à mettre en place, et ce même dans un projet existant. J’utilise cette technique principalement sur les fichiers CSS et JavaScript, mais je ne l’utilise pas encore pour les images.

La mise à jour des ressources est garantie au changement de contenu.

  • Qu’en pensez-vous ?
  • Comment gérez-vous le cache sur vos sites ?

14 réflexions au sujet de « Comment gérer simplement la mise en cache navigateur ? »

  1. Je n’ai pas bien compris le fonctionnement. Si aucune requête de vérification n’est faite et que l’url ne change pas, pourquoi le navigateur irait retélécharger la ressource ?

    • Si tu n’indiques pas au navigateur une date d’expiration (ici 1 mois), il va effectuer systématiquement une requête HTTP afin de vérifier si le fichier est identique à celui présent dans son cache. Si c’est le même il ne télécharge pas le contenu, mais il aura perdu du temps à faire une requête alors qu’il aurait pu faire autre chose à la place.

      Ce système force le navigateur à utiliser son cache, mais permet aussi de mettre à jour le fichier quand le développeur le souhaite.

  2. Petite reponse pour te dire que complique un peut la vie 😉

    En effet, si tu veux facilment mettre à jour le cache navigateur , un simple fichier.js?v=1 suffit, il te suffit donc d’une constante et le tour est joué.

    Mais dans les deux cas la situation finale est la même, ta solution avec le mod_rewrite étant plus esthétique.

    Voilà, bonne journée!

    ps: pas facile d’écrire dans un train!

    • Tout à fait, c’est une possibilité, surtout si le mod_rewrite ne peut être utilisé.

      Mais je n’aime pas cette technique, je ne la trouve pas esthétique. Je préfère garder les paramètres d’URL à leur utilisation normal 😉

      Mais bon, ce n’est que mon point de vu.
      Je vais d’ailleurs mettre à jour l’article. En effet, ma technique parasite les chemins des URL relatives (numéro de version considéré comme un répertoire), je vais donc le mettre dans le nom du fichier.

  3. Salut Blount,
    Une petite amélioration/astuce pour ton script.
    Tu peux, au lieu de manuellement définir la version de ton fichier avec
    define('APPLICATION_REV', '1.0');
    récupérer via php la date de ton fichier:
    $date_fichier = stat("styles.css");
    define('APPLICATION_REV', $date_fichier);

    Ainsi, tu es sur de ne jamais oublier de modifier ton numéro de version :)

    • Cette solution n’est pas fiable du tout.

      Si tu regardes bien la documentation, le résultat de la fonction « stat » est mis en cache.

      De plus, un accès disque est nécessaire pour chaque fichier ressource, tu ne peux pas te baser sur un seul fichier.

      Je le déconseille donc.

  4. Ping : Passer des tableaux (table) aux blocs (div) | Programmation Web

  5. Cette solution me plait beaucoup et a l’air simple à mettre en place (ce que je cherchais). Je m’étonne de ne pas voir de RewriteEngine on, il n’est plus nécessaire dans un clause ??

    Existe-t-il une solution de mise en cache des IMAGES simple dans le cas où le mod_expires ne serait pas activé ?

  6. Bonjour,

    J’ai tous lu, même les commentaires, mais je ne vois pas dans quel fichier faire le changement pour la première étape.

    Pour le deuxième étape j’ai compris que c’est dans le fichier .htaccess

    Serge

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Notifiez-moi des commentaires à venir via email. Vous pouvez aussi vous abonner sans commenter.