WordPress, gestion du cahe au niveau du serneur

Gestion du cache des fichiers statiques dans WordPress

La gestion du cache des fichiers statiques dans WordPress relève d’une stratégie devant prendre en compte la performance, la sécurité et la cohabitation des différentes ressources sur la plateforme. Alors que le cache-busting est nativement supporté par le CMS, nous devons coupler l’utilisation des filtres WordPress et du fichier de configuration htaccess d’Apache pour contrôler plus précisément les requêtes adressées par les navigateurs à nos fichiers CSS et JavaScript.

Là où une part des problèmes de performance rencontrés sur les sites internet peuvent être résolus en améliorant la gestion du cache au niveau serveur, il peut être utile de comprendre son mécanisme avant de se ruer sur le premier plugin payant. Quoi qu’il en soit, en cas de doute, n’hésitez pas à demander son avis à votre webmaster favori 😉 .

La miss en cache

La mise en cache des documents dans leur ensemble (HTML, JS, fichiers images, vidéo, Flash, etc.) est initiée au niveau du serveur (dans notre cas Apache). L’insertion d’une partie variable à la fin de leur URL permet d’indiquer au navigateur s’il doit retourner les chercher sur le serveur ou s’il peut piocher dans sa mémoire temporaire et s’éviter des requêtes coûteuses.

Query string : traitement dynamique des requêtes

La query string (ou chaîne de requête) est la partie dans l’URL qui ne correspond ni au nom ni une structure arborescente menant au fichier. Elle ajoute en général des informations à la requête pour permettre un traitement dynamique des contenus proposés en réponse (des variables), mais sert aussi à la gestion du cache des fichiers statiques. Cette partie de l’URL débute par un point d’interrogation « ? » et est composée d’une donnée simple ou d’une ou plusieurs paires clé=valeur. Si plusieurs paires sont présentes, elles sont séparées par une esperluette « & » à partir de la 2e rencontrée.

Une query string pour les fichiers statiques : le cache-busting

Pour les fichiers CSS et JavaScript, un identifiant est souvent ajouté à l’URL de la ressource comme casseur de cache (cache-breaker). La mise en cache des fichiers statiques (comme des autres ressources) étant définie au niveau du serveur, une modification de la chaîne de requête (query string) indique au serveur d’aller chercher directement la ressource au lieu de piocher dans son cache si cette chaîne diffère de ce qu’il connaît. Ce procédé garantit l’actualisation des données avant la fin de la période d’expiration prédéfinie.

Les directives Apache d’expiration du cache du client

Le module Apache mod_expires.c permet de fixer une date d’expiration du cache client, par défaut, et en fonction du type de document. Là où il est de coutume de paramétrer une expiration du cache à un mois (donnée variable) pour les ressources, média (images ou vidéo), polices et flux RSS, on considère que les mises à jour des fichiers de stylisation comme celles des scripts clients peuvent être moins fréquentes. On préfère fixer une période de validité du cache plus grande pour les ressources CSS et JavaScript, forçant le navigateur à recharger son cache (et actualiser son affichage) sur un terme plus long. Mais en cas d’update de sur ce type de fichiers avant le terme butoir, le navigateur n’a aucun moyen d’en prendre connaissance à moins d’un changement de nom (et d’URL) de la ressource. Au niveau d’un fichier de configuration .htaccess, on commence par activer le module (ExpiresActive on), on définit la durée par défaut de mise en cache pour tous les documents servis, avec une directive de type ExpiresDefault « access plus <durée> ». On attribue ensuite un terme plus précis en fonction du type MIME du document avec ExpiresByType, définissant ainsi la directive max-age de l’en-tête Cache-control pour ce type. Le code suivant, à défaut d’être exhaustif permet de visualiser ces directives :


<IfModule mod_expires.c>
ExpireActive on
ExpiresDefault "access plus 1 month"
ExpiresByType application/rss+xml "access + 0 seconds"
ExpiresByType text/css "access plus 6 month"
ExpiresByType application/javascript "access plus 6 month"
</IfModule>

On autorise (ou pas) le partage de la mise en cache (utile pour les serveurs proxy) des documents avec :


<IModule mod_headers.c>
<FileMatches "\.( |css)$">
	Header set Cache-control "public"
</FileMatches>
<FileMatches "\.js$">
	Header set Cache-control "private"
</IfModule>

On initialize donc bien la mise en cache au niveau du serveur Apache. Un plugin de cache fera exactement ce travail. Ou mai …

Problématique de la mise en cache des fichiers statiques

Le problème avec l’utilisation des query strings est multiple. WordPress utilise nativement la version courante du core pour approvisionner la query string. Les URL des fichiers d’extensions .css et .js sont donc souvent suivies de chaînes de type ?ver=4.2.2. On retrouve ces URL dans les attributs href ou src des balises <link> et <script> :


<link rel="stylesheet" type="text/css" 
href="http://www.mon-domaine.fr/wp-content/themes/mytheme/css/mycss.css?ver=4.4.2" />

Tout va bien tant que la version du CMS est à jour, mais si le gestionnaire du site (le client) n’assure pas ses mises à jour, ce numéro de version devient une information utile pour trouver les failles de sécurités du système, et cette partie variable restant inchangée, sa fonctionnalité de « briseur de cache » s’en trouve limitée. De plus, la plupart des contenus statiques ne sont pas mis en cache par les proxys, ce qui rend inutile ce procédé. Enfin, ces ressources pouvant être assez nombreuses sur un site, cela peut alourdir le processus de mise en cache des navigateurs individuels, pouvant amener un résultat inverse de ce que l’on attendait. Une analyse du site avec un outil en ligne tel que http://gtmetrix.com suffit pour se rendre compte du problème généré :


"Resources with a "?" in the URL are not cached by some proxy caching servers. 
Remove the query string and encode the parameters into the URL for the following resources :"

Ici, l’outil nous demande clairement de faire passer le casseur de cache dans l’URL du fichier (avant son extension).

Il devient donc évident que la gestion du cache des fichiers statiques de WordPress ne peut être abandonnée à ce seul mécanisme

3 Solutions pour la régler le problème de mise en cache des fichiers statiques sous WordPress

Tout va dépendre de vos intensions. Si vous utilisez peu de ressources statiques et qu’elles sont bien optimisées, vous pouvez vouloir supprimer leur mise en cache. C’est un peu radical, mais pourquoi pas.

Suppression de la mise en cache des fichiers statiques de WordPress

Pour empêcher le CMS d’ajouter son numéro de version à l’adresse de chaque fichier statique on va à nouveau utiliser les filtres WordPress. De la même manière que nous utilisons les actions spécifiques wp_enqueue_style et wp_enqueue_script pour inclure proprement les balises <link> et <script> à nos pages nous allons utiliser deux filtres pour filtrer les sources de ces fichiers : « script_loader_src » et « style_loader_src« .

C’est naturellement que le callback (fonction de retour) du filtre recevra la source, l’URL complète de chaque ressource statique liée. En accédant directement à cette chaîne, on peut la découper en segments séparés par le point d’interrogation « ? » (qui marque le début de la query string) en utilisant la fonction PHP explode(). Le premier de ces segments correspondra donc à l’adresse du fichier CSS ou fichier JavaScript, les autres aux paires «clé=valeur» rencontrées. Pour rappel, la fonction PHP explode() prend en arguments la chaîne servant de délimiteur, puis la chaîne à découper :


function _remove_script_version( $src ){
	$parts = explode( '?', $src );
	 return $parts[0];
}
add_filter( 'script_loader_src', '_remove_script_version', 10, 1 ); 
add_filter( 'style_loader_src', '_remove_script_version', 10, 1 ); 

En ne retournant que la première entrée du tableau créé par explode(), on s’assure d’éliminer la chaîne de requête.On utilisera ces deux filtres et leur calllback directement dans le fichier functions.php. Mais cette solution étant un tantinet radicale, et on peut souhaiter conserver son casseur de cache pour gagner en performance. La solution consiste à utiliser la date de modification du fichier, donc la fonction PHP filemtime().

La partie qui suit est inspirée d’articles comme celui de Devin Walker sur la gestion de la mise en cache des fichiers CSS ET JavaScipt de WordPress ou celui de Chris Covier sur CSS-Tricks : Strategies for Cache-Busting CSS. Mais les méthodes décrites n’apportant pas une solution universelle, je me suis employé à trouver une alternative acceptable pour optimiser la mise en cache des fichiers statiques sur WordPress.

Remplacer le numéro de version par la date de modification avec filemtime()

Attention, pour mettre en œuvre cette solution, vous devez passer à filemtime() le chemin menant à votre ressource, et pas son URI. Vous lui passerez en argument le chemin récupéré avec get_template_directory() au lieu de pas get_template_directory_uri() sous peine d’échec de la fonction.

La fonction PHP filemtime( $path ) retourne le timestamp correspondant à la dernière modification du fichier. Le quatrième paramètre de wp_enqueue_style() passé à false ou une chaîne vide entraînera l’utilisation de la version courante de WordPress pour la query string, et rien du tout en cas de passage à NULL. On va utiliser ce 4e argument pour injecter le timestamp fourni par filemtime() en guise de version.


function my_theme_enqueue_scripts(){
	wp_enqueue_style( 
		'my-css', 
		get_stylesheet_directory_uri().'/css/my-css.css',
		false,
		filemtime( get_stylesheet_directory().'/css/my-css.css' ),
		'all' );
	wp_enqueue_script( 
		'my-script',
		get_stylesheet_directory_uri().'/js/my-css.js',
		array( jquery ),
		filemtime( get_stylesheet_directory().'/js/my-css.js ),
		true );
}

On intègre ces ressources en passant le hook « wp_enqueue_scripts » :


	add_action( "wp_enqueue_scripts", "my_theme_enqueue_scripts" );

La solution précédente permet d’insérer une version de cache ne trahissant pas la version du CMS, mais conserve une query string derrière le nom des ressources statiques. On va donc chercher à transformer nos URL internes pour faire passer ce timestamp avant l’extension « .css » ou « .js » de nos fichiers.

Injection du numéro de version avant l’extension des fichiers statiques

Cette dernière solution consiste à utiliser nos filtres « style_loader_src » et « script_loader_src » pour réinjecter notre numéro de version avant l’extension du fichier, et à modifier notre fichier .htaccess pour établir une correspondance entre nos URL internes et nos fichiers de feuilles de styles et JavaScript. Une nouvelle problématique va s’imposer à nous : la réécriture des URL des fichiers CSS et JavaScript peut impacter les plugins tierces parties et les composants de thèmes premium. Il faudra dans ce cas, restreindre le remplacement effectué au niveau du script, aux fichiers contenus dans le répertoire du thème actif.

Depuis un plugin ou dans le fichier functions.php

On commence par cibler la fin de l’URL de la ressource.On en profite pour capturer l’extension du fichier, et la query string. L’expression régulière sera donc :

	"#\.(css|js)?ver=(.+)$#"

Analyse rapide de l’expression régulière

On cible une chaîne se finissant par un point « . », suivi d’une extension « js » ou « css » (que l’on capture), d’un délimiteur de query string et d’une variable de version « ?ver= », et enfin, de quoi que ce soit servant de version que l’on capture aussi « (.*)$ ». En ciblant exactement ce paterne, on s’assure que le remplacement n’impactera que la partie de la chaîne corespondand à ce masque. La fonction PHP preg_replace( $pattern, $replacement, $src ) conservera intacte la partie de l’URL qui ne correspond pas au paterne. On peut alors intervertir l’extension et le numéro de version en récupérant les captures dans $1 et $2, en les séparant avec un point « . ». Si le dashboard d’administration est affiché, on laisse tranquiles nos urls.


fucntion move_script_version( $src ){
	if( is_admin() ){
		return $src;
	}
	return preg_replace( "#\.(css|js)\?ver=(.*)$#", '.$2.$1', $src );
}

: Si vous souhaitez comprendre le mécanisme de l’expression régulière précédente, tester une fonction similaire dans un fichier externe :


function move_script_version( $src ){
	if( preg_match( "#\.(css|js)\?ver=(.*)$#", $src, $matches ) ){
		return $matches;
	}
}
echo '<pre>'.print_r(
	move_src_version( 
		'http://www.mydomain.fr/wp-content/themes/concept/css/my-css.css?ver=12546987' )
	, 1 ).'</pre>';

Mise en correspondance des URL internes et des fichiers statiques dans .htaccess

Il faut maintenant établir notre correspondance entre nos nouvelles URL et les noms de fichiers. On cherche donc au niveau du serveur (fichier .htaccess), à réécrire les URL de nos fichiers statiques pour les renvoyer vers leur équivalent amputé du cache breaker précédemment injecté avant l’extension. Le masque « (.*)\.(.*)\.(css|js) » sera donc transformé en « $1.$3 » :


RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEXT_FILENAME} !-f
RewriteFile ^(.+)\.(.+)\.(css|js)$ $3.$1 [L]

Ici, on capture les 3 composantes des requêtes vers les fichiers CSS et JavaScript pour les recomposer en éliminant la partie correspondant au cache précédemment inclus devant l’extension. Si vous avez du mal à comprendre, n’hésitez pas à visualiser le code source de votre page dans le navigateur (click droit sur la page -> code source) et observez vos URL de fichiers CSS, vous verrez alors la correspondance à établir entre le masque de la directive RewriteRule et vos adresses de fichiers.

Un commentaire de l’article de Devin Walker met l’accent sur la possibilité d’impact non souhaité de la réécriture d’URL Apache sur les ressources de thèmes ou de plugins (notamment les fichiers minifiés). La solution préconisée est de modifier le masque dans la directive RewriteRule du .htaccess afin d’épargner ces dernières :


	RewriteRule ^([^.]+)\.(.+)\.(js|css)$ $1.$3 [L]

J’ai pour ma part remarqué que cette modification n’était pas suffisante pour empêcher la réécriture d’un grand nombre de fichiers sensibles. Le résultat est sans appel : une cascade d’erreur 404 et une explosion désastreuse du design.

Limitation de la recomposition d’URL pour les fichiers statiques

Ma solution consiste donc à n’insérer le cache-breaker avant l’extension, que de manière ciblée. C’est-à-dire pour les fichiers créés spéficiquement pour le thème actif, que ce soit un thème construit (from scratches) ou un thème enfant. Notre callback pour les filtres « script_loader_src » et « style_loader_src » prend alors la forme suivante :


function remove_static_files_version( $fileurl ){
	if( is_admin() ) return $fileurl;
	if( !preg_match( "#/mytheme/#", $fileurl ) ){
		return $fileurl;
	}
	return preg_replace( "/\.(js|css)\?ver=(.+)$/", ".$2.$1",$fileurl );
}

Si dans l’adresse du fichier, aucune correspondance n’est trouvée avec le répertoire /mytheme/, je retourne l’URL non modifiée. Mais vous pouvez aussi décider ici de supprimer purement et simplement la query string c’est à vous de voir :


	if( !preg_match( "#/mytheme/#", $fileurl ) ){
		$parts = explode( "?", $fileurl );
		return $parts[ 0 ];
	}

Dans le cas contraire, j’opère une recomposition de l’URL qui elle, sera traitée avec succès au niveau du serveur.

Gestion du cache des fichiers statiques de WordPress : contexte et performances

Un dernier mot pour vous déconseiller bien sûr de mettre en place votre système de cache en phase de développement (vous seriez obligé de forcer le rechargement de la page (reload) à chaque visualisation. Enfin, pesez le pour et le contre dans votre gestion du cache des fichiers statiques WordPress. La multiplication de ressources lourdes ou mal optimisées accompagnéed’une suppression de leur mise en cache pourrait augmenter inutilement le nombre de requêtes vers le serveur, ralentissant les performances du site (et Google semble ne pas trop aimer). À contrario, une mise en cache trop fréquente d’un grand nombre de ressources ralentit le processus, produisant un résultat inverse en terme de performance. La bonne pratique se situera certainement entre les deux, et à l’appréciation du contexte par le webmaster 😉 .

Laisser un commentaire

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