PHP : envoyer les bons en-têtes HTTP de cache

La mise en cache HTTP est l’un des leviers les plus efficaces pour accélérer un site. Elle évite au navigateur de télécharger plusieurs fois les mêmes ressources et permet aux CDN de servir des réponses sans solliciter l’origine à chaque requête.

Mais un mauvais en-tête de cache peut aussi créer des bugs très pénibles : page privée visible trop longtemps, panier WooCommerce figé, ancien CSS conservé, API qui renvoie des données périmées, ou fichier JavaScript jamais mis à jour. Bref, le cache est un accélérateur. Pas une baguette magique. Et il mord si on le configure mal.

Voici comment envoyer les bons en-têtes HTTP avec PHP, selon le type de contenu : fichiers statiques, HTML dynamique, API, pages privées, WordPress et WooCommerce.

Distingo, le livret à 2%

Le rôle des en-têtes HTTP de cache

Quand un navigateur demande une ressource, le serveur peut indiquer comment cette réponse doit être conservée, réutilisée ou revalidée.

Les principaux en-têtes concernés sont :

En-têteRôle
Cache-ControlDéfinit les règles de cache modernes
ExpiresAncien mécanisme basé sur une date d’expiration
ETagIdentifiant de version permettant la revalidation
Last-ModifiedDate de dernière modification utilisée pour revalider
VaryIndique que la réponse varie selon certains en-têtes de requête

Aujourd’hui, Cache-Control est le plus important. Expires peut rester utile pour compatibilité, mais il ne doit plus piloter toute votre stratégie.

Les directives Cache-Control essentielles

Voici les directives que vous croiserez le plus souvent.

DirectiveSignification
publicLa réponse peut être stockée par le navigateur et les caches partagés
privateLa réponse ne doit être stockée que par le navigateur de l’utilisateur
max-age=3600La réponse reste fraîche pendant 3600 secondes
s-maxage=3600Durée spécifique pour les caches partagés comme CDN/proxy
no-cacheLa réponse peut être stockée, mais doit être revalidée avant réutilisation
no-storeLa réponse ne doit pas être stockée
must-revalidateUne réponse périmée doit être revalidée avant usage
immutableLa ressource ne changera pas pendant sa durée de fraîcheur
stale-while-revalidateUn cache peut servir une réponse périmée pendant qu’il la revalide

La nuance importante : no-cache ne veut pas dire “ne pas cacher”. Cela veut dire “tu peux stocker, mais tu dois revalider avant de réutiliser”. Si vous voulez empêcher tout stockage, utilisez no-store.

Kinsta: Premium Managed WordPress hosting

Cas 1 : contenu dynamique qui ne doit pas être stocké

Pour une page contenant des données sensibles ou personnelles, utilisez no-store.

<?php

header( 'Cache-Control: no-store, max-age=0' );
header( 'Pragma: no-cache' );
header( 'Expires: 0' );Langage du code : HTML, XML (xml)

Utilisez ce type de header pour :

  • pages de compte utilisateur ;
  • zones admin ;
  • checkout ;
  • panier ;
  • pages contenant des informations personnelles ;
  • réponses avec tokens ou données privées.

Dans WordPress, utilisez plutôt nocache_headers() quand vous êtes dans le cycle WordPress. La fonction existe justement pour envoyer plusieurs headers anti-cache compatibles avec différents navigateurs et caches intermédiaires.

<?php
nocache_headers();Langage du code : HTML, XML (xml)

Cas 2 : HTML dynamique avec revalidation

Pour une page HTML publique mais susceptible de changer, vous pouvez autoriser le stockage tout en forçant une revalidation.

<?php
header( 'Cache-Control: public, no-cache, must-revalidate' );Langage du code : HTML, XML (xml)

Cette réponse peut être stockée, mais le navigateur ou le cache doit vérifier auprès du serveur avant de la réutiliser. C’est utile si vous voulez profiter des validations conditionnelles avec ETag ou Last-Modified.

Pour des pages publiques très fréquentées, un cache court peut être plus efficace :

<?php
header( 'Cache-Control: public, max-age=300, stale-while-revalidate=60' );Langage du code : HTML, XML (xml)

Ici, la réponse est fraîche pendant cinq minutes. Ensuite, un cache compatible peut servir l’ancienne réponse pendant une minute pendant qu’il la revalide. MDN décrit justement stale-while-revalidate comme une directive permettant de réutiliser une réponse périmée pendant la revalidation. :contentReference[oaicite:2]{index=2}

Kinsta: Premium Managed WordPress hosting

Cas 3 : fichiers statiques versionnés

Pour les assets versionnés — CSS, JavaScript, images, polices — utilisez un cache long.

Cache-Control: public, max-age=31536000, immutableLangage du code : PHP (php)

Ce header est idéal si l’URL change quand le fichier change :

/assets/app.8f3a91c.css
/assets/theme.2025-10-14.js
/uploads/logo-v2.avif

Il est dangereux si vous servez toujours le même nom de fichier, par exemple :

/assets/style.css

Si style.css change mais que son URL reste identique, un cache long avec immutable peut garder l’ancienne version trop longtemps. La bonne stratégie est donc : cache long uniquement avec URLs versionnées.

Cas 4 : API JSON publique

Pour une API publique qui renvoie des données peu sensibles, vous pouvez choisir un cache court.

<?php
header( 'Content-Type: application/json; charset=utf-8' );
header( 'Cache-Control: public, max-age=60, stale-while-revalidate=30' );

echo json_encode(
	array(
		'status' => 'ok',
		'time'   => time(),
	),
	JSON_THROW_ON_ERROR
);Langage du code : HTML, XML (xml)

Pour une API privée, utilisez plutôt private ou no-store selon le niveau de sensibilité.

<?php
header( 'Content-Type: application/json; charset=utf-8' );
header( 'Cache-Control: no-store, max-age=0' );Langage du code : HTML, XML (xml)
Kinsta: Premium Managed WordPress hosting

Cas 5 : téléchargement de fichier généré par PHP

Pour un fichier généré à la demande, comme un export CSV personnalisé, empêchez le stockage si le contenu dépend de l’utilisateur.

<?php
$filename = 'export-' . gmdate( 'Y-m-d' ) . '.csv';

header( 'Content-Type: text/csv; charset=utf-8' );
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
header( 'Cache-Control: no-store, max-age=0' );
header( 'Pragma: no-cache' );
header( 'Expires: 0' );

echo "id,email\n";
echo "1,client@example.com\n";Langage du code : HTML, XML (xml)

Pour un fichier public identique pour tout le monde, vous pouvez au contraire autoriser le cache. Tout dépend du contenu, pas seulement de l’extension.

Ajouter Last-Modified en PHP

Last-Modified indique la date de dernière modification d’une ressource. Le navigateur peut ensuite envoyer If-Modified-Since pour demander si le contenu a changé.

Exemple simple pour un fichier servi par PHP :

<?php
$file = __DIR__ . '/data/public.json';

if ( ! is_file( $file ) ) {
	http_response_code( 404 );
	exit;
}

$last_modified = filemtime( $file );
$etag          = '"' . md5_file( $file ) . '"';

header( 'Content-Type: application/json; charset=utf-8' );
header( 'Cache-Control: public, max-age=300, must-revalidate' );
header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', $last_modified ) . ' GMT' );
header( 'ETag: ' . $etag );

$if_none_match = $_SERVER['HTTP_IF_NONE_MATCH'] ?? '';
$if_modified_since = $_SERVER['HTTP_IF_MODIFIED_SINCE'] ?? '';

if (
	$if_none_match === $etag
	|| strtotime( $if_modified_since ) === $last_modified
) {
	http_response_code( 304 );
	exit;
}

readfile( $file );Langage du code : HTML, XML (xml)

Cette logique permet de répondre 304 Not Modified si le navigateur possède déjà la bonne version. La réponse est alors beaucoup plus légère.

ETag ou Last-Modified ?

ETag et Last-Modified servent tous les deux à revalider une réponse. Ils ne répondent pas exactement au même besoin.

En-têteAvantageLimite
Last-ModifiedSimple, basé sur une datePrécision limitée, dépend du timestamp
ETagIdentifie précisément une versionPeut être mal configuré en multi-serveur

Pour un site classique, Last-Modified est souvent suffisant. Pour une ressource générée ou versionnée précisément, ETag peut être très utile. MDN documente ETag comme un validateur permettant au serveur de déterminer si une ressource a changé. :contentReference[oaicite:3]{index=3}

Attention à Vary

Vary indique qu’une réponse peut varier selon certains en-têtes de requête. C’est indispensable dans certains cas, mais cela peut fragmenter le cache.

Exemples courants :

Vary: Accept-EncodingLangage du code : HTTP (http)

ou :

Vary: Accept-LanguageLangage du code : HTTP (http)

Évitez de mettre Vary: Cookie sur des pages publiques si vous voulez qu’un CDN les cache efficacement. Cela peut créer une version par cookie, donc quasiment annuler le bénéfice du cache partagé.

PHP : envoyer les headers avant toute sortie

En PHP, les headers doivent être envoyés avant tout contenu. Pas d’espace avant <?php, pas d’echo, pas de HTML, pas de BOM UTF-8, pas de var_dump avant les headers.

<?php
header( 'Cache-Control: public, max-age=300' );
echo 'Contenu';Langage du code : HTML, XML (xml)

Sinon, vous risquez l’erreur classique :

Warning: Cannot modify header information - headers already sentLangage du code : HTTP (http)

Si vous développez un plugin WordPress ou un contrôleur PHP, envoyez les headers au bon moment dans le cycle d’exécution, avant la sortie.

WordPress : ne pas envoyer les mêmes headers partout

Sur WordPress, il ne faut pas appliquer une politique de cache uniforme à tout le site. Une page d’article publique, une page panier WooCommerce, une page compte client et une réponse REST privée n’ont pas les mêmes contraintes.

Exemple de logique prudente dans un plugin ou un mu-plugin :

<?php
/**
 * Plugin Name: SkyMinds HTTP Cache Headers
 * Description: Adds conservative HTTP cache headers for public WordPress pages.
 * Author: Matt Biscay
 * Version: 1.0.0
 */

declare(strict_types=1);

defined( 'ABSPATH' ) || exit;

add_action( 'send_headers', 'skyminds_send_public_cache_headers' );

/**
 * Send conservative cache headers for public, anonymous front-end pages.
 *
 * @return void
 */
function skyminds_send_public_cache_headers(): void {
	if ( is_admin() || is_user_logged_in() || wp_doing_ajax() || wp_is_json_request() ) {
		return;
	}

	if ( function_exists( 'is_cart' ) && ( is_cart() || is_checkout() || is_account_page() ) ) {
		nocache_headers();
		return;
	}

	if ( is_singular() || is_home() || is_front_page() || is_archive() ) {
		header( 'Cache-Control: public, max-age=300, stale-while-revalidate=60' );
	}
}Langage du code : HTML, XML (xml)

Ce snippet reste volontairement conservateur. Il évite les utilisateurs connectés, l’admin, AJAX, JSON et les pages WooCommerce sensibles.

WooCommerce : prudence maximale

Avec WooCommerce, le cache HTTP doit être précis. Ne cachez jamais comme une page statique :

  • le panier ;
  • le checkout ;
  • le compte client ;
  • les endpoints de paiement ;
  • les réponses personnalisées par cookie ou session ;
  • les prix dynamiques si leur affichage dépend du visiteur.

En revanche, vous pouvez souvent mettre un cache court sur les pages catalogue publiques pour les visiteurs anonymes : catégories produits, tags produits, pages marque, pages de listing. Mais testez le panier, les fragments, les prix et les variations. WooCommerce adore rappeler que “ça marche sur la home” ne veut pas dire “ça marche en checkout”.

CDN : distinguer navigateur et cache partagé

Un navigateur et un CDN ne jouent pas exactement le même rôle. max-age concerne tous les caches, tandis que s-maxage cible les caches partagés comme les CDN et proxys.

Exemple utile pour une page publique :

Cache-Control: public, max-age=60, s-maxage=600, stale-while-revalidate=60Langage du code : PHP (php)

Ici, le navigateur garde la réponse fraîche pendant une minute, mais le CDN peut la garder dix minutes. C’est souvent intéressant pour WordPress : le visiteur garde peu, l’edge garde plus.

Vérifiez toutefois la documentation de votre CDN. Cloudflare, Fastly, Bunny, CloudFront et les hébergeurs managés peuvent interpréter ou compléter les headers avec leurs propres règles.

Tester les headers avec curl

Pour vérifier les headers d’une page :

curl -I https://www.example.com/Langage du code : JavaScript (javascript)

Pour vérifier une ressource statique :

curl -I https://www.example.com/assets/app.cssLangage du code : JavaScript (javascript)

Pour voir les headers avec compression demandée :

curl -I -H "Accept-Encoding: br,gzip" https://www.example.com/Langage du code : JavaScript (javascript)

Pour tester une revalidation conditionnelle avec ETag :

curl -I -H 'If-None-Match: "votre-etag"' https://www.example.com/data.jsonLangage du code : JavaScript (javascript)

Pour tester avec If-Modified-Since :

curl -I -H "If-Modified-Since: Wed, 15 May 2024 10:00:00 GMT" https://www.example.com/data.jsonLangage du code : JavaScript (javascript)

Recettes rapides selon le type de réponse

Type de contenuHeader recommandé
HTML public WordPresspublic, max-age=300, stale-while-revalidate=60
Page privéeno-store, max-age=0
API privéeno-store, max-age=0
API publique courtepublic, max-age=60, stale-while-revalidate=30
CSS/JS versionnépublic, max-age=31536000, immutable
Images versionnéespublic, max-age=31536000, immutable
HTML avec CDNpublic, max-age=60, s-maxage=600, stale-while-revalidate=60
Réponse à revaliderpublic, no-cache, must-revalidate

Erreurs fréquentes

  • Mettre no-store sur tout le site et tuer le cache navigateur.
  • Mettre immutable sur des fichiers non versionnés.
  • Cacher des pages WooCommerce personnalisées par cookie.
  • Confondre no-cache et no-store.
  • Oublier que PHP doit envoyer les headers avant toute sortie.
  • Ajouter Vary: Cookie sans comprendre l’impact sur le CDN.
  • Faire confiance au navigateur sans tester avec curl -I.
  • Appliquer une règle globale au lieu de raisonner par type de contenu.

Checklist avant publication

  • Identifier le type de contenu : public, privé, statique, dynamique, API.
  • Choisir public, private, no-cache ou no-store.
  • Définir une durée cohérente avec max-age.
  • Utiliser s-maxage si un CDN doit garder plus longtemps que le navigateur.
  • Ajouter immutable seulement pour des URLs versionnées.
  • Vérifier les pages connectées et privées.
  • Tester panier, checkout et compte client sur WooCommerce.
  • Tester les headers avec curl -I.
  • Vérifier le comportement CDN depuis une navigation anonyme.

Articles liés sur SkyMinds

À retenir

Les bons en-têtes de cache dépendent du contenu. Il n’existe pas un header magique à mettre partout.

Pour des fichiers statiques versionnés, utilisez un cache long :

Cache-Control: public, max-age=31536000, immutableLangage du code : PHP (php)

Pour des pages privées ou sensibles, interdisez le stockage :

Cache-Control: no-store, max-age=0Langage du code : HTTP (http)

Pour des pages publiques dynamiques, utilisez un cache court et mesuré :

Cache-Control: public, max-age=300, stale-while-revalidate=60Langage du code : PHP (php)

Le cache HTTP est puissant quand il respecte la nature de la réponse. Cachez longtemps ce qui est versionné. Revalidez ce qui peut changer. Ne stockez jamais ce qui est privé. Voilà, le navigateur est content, le CDN aussi, et le serveur respire un peu.

Sources

Demandez à l'IA son opinion
Gravatar for Matt Biscay

Je suis Matt Biscay, développeur WordPress & WooCommerce certifié chez Codeable, administrateur système et enseignant.

J’aide les entreprises à créer, optimiser et fiabiliser leurs sites WordPress avec une approche technique propre : performance, sécurité, maintenance, développement sur mesure et résolution de problèmes complexes.

Sur Skyminds, je partage des tutoriels WordPress, WooCommerce, Linux et administration système, avec des solutions testées sur des cas réels et pensées pour durer.

Découvrez mes services WordPress et WooCommerce.

Laisser un commentaire