# Mettre en cache les réponses de l’API REST WordPress côté serveur

> Chaque requête REST relance des requêtes SQL. Sur un front à fort trafic, le cache côté serveur devient vite indispensable : voici comment le mettre en place.

- Auteur : WordPress Développement
- Publié le : 2020-12-15
- Mis à jour le : 2026-09-30
- Catégorie : Headless &amp; API
- URL : https://www.wpmoderne.fr/headless/cache-reponses-api-rest-wordpress-cote-serveur/

## L’essentiel

- Cache applicatif avec WP REST Cache pour les routes publiques
- En-têtes HTTP standards pour le cache CDN
- Invalidation ciblée à la publication ou modification

Un client dont le front Gatsby interrogeait l'API REST WordPress à chaque build a vu son temps de build passer de 40 secondes à plus de 3 minutes une fois son catalogue de contenus dépassé les 800 articles. La cause : chaque requête vers `/wp-json/wp/v2/posts` déclenchait une série de requêtes SQL non négligeable (articles, termes de taxonomie associés, méta-données), sans aucune mise en cache entre deux appels identiques. Ce problème touche aussi bien les builds statiques que les fronts avec rendu à la demande.

Trois niveaux de cache peuvent intervenir sur une route REST : le cache applicatif (au niveau de WordPress lui-même), les en-têtes HTTP de cache, et un cache CDN en amont du serveur. Cet article couvre les deux premiers en détail, le troisième de façon plus rapide car il dépend fortement de l'hébergeur.

## Cache applicatif avec WP REST Cache

L'extension WP REST Cache intercepte les réponses des routes REST publiques et les stocke dans le cache d'objets de WordPress (transients par défaut, ou Redis/Memcached si configuré). À la requête suivante identique, la réponse est servie depuis le cache sans recalcul.

> L'essentiel à retenir : Cache applicatif avec WP REST Cache pour les routes publiques ; En-têtes HTTP standards pour le cache CDN ; Invalidation ciblée à la publication ou modification

Le principe de l'extension est celui que l'on retrouve dans tout cache de réponses : une clé dérivée de l'adresse demandée et de ses paramètres, une durée de vie, et un vidage à la modification du contenu. Pour un site standard, l'installer et régler la durée de vie suffit. Comprendre ce qu'elle fait reste utile, parce que les mêmes rouages servent à combler un cas particulier ou à diagnostiquer un cache qui sert des données périmées. Voici une version minimale, écrite avec les filtres du cœur `rest_pre_dispatch` et `rest_post_dispatch`, disponibles depuis WordPress 4.4.

## Un cache minimal avec rest_pre_dispatch et rest_post_dispatch

Le filtre `rest_pre_dispatch` reçoit `null` avant que la route ne s'exécute : si un filtre retourne autre chose, cette valeur devient la réponse et la route n'est jamais appelée. Le filtre `rest_post_dispatch` reçoit la réponse construite et permet de la stocker. Il suffit de relier les deux :

```
add_filter( 'rest_pre_dispatch', 'wpm_cache_rest_lire', 10, 3 );
add_filter( 'rest_post_dispatch', 'wpm_cache_rest_ecrire', 10, 3 );

function wpm_cache_rest_eligible( WP_REST_Request $requete ) {
	return 'GET' === $requete->get_method()
		&& 0 === strpos( $requete->get_route(), '/wp/v2/' )
		&& ! is_user_logged_in();
}

function wpm_cache_rest_cle( WP_REST_Request $requete ) {
	$params = $requete->get_query_params();
	ksort( $params );
	$version = (int) get_option( 'wpm_cache_rest_version', 1 );

	return 'wpm_rest_' . md5( $version . '|' . $requete->get_route() . '|' . wp_json_encode( $params ) );
}

function wpm_cache_rest_lire( $resultat, $serveur, $requete ) {
	if ( null !== $resultat || ! wpm_cache_rest_eligible( $requete ) ) {
		return $resultat;
	}

	$entree = get_transient( wpm_cache_rest_cle( $requete ) );
	if ( false === $entree ) {
		return $resultat;
	}

	$reponse = new WP_REST_Response( $entree['donnees'], $entree['statut'], $entree['en_tetes'] );
	$reponse->add_links( $entree['liens'] );
	$reponse->header( 'X-Cache-WPM', 'HIT' );

	return $reponse;
}

function wpm_cache_rest_ecrire( $reponse, $serveur, $requete ) {
	if ( ! wpm_cache_rest_eligible( $requete ) || 200 !== $reponse->get_status() ) {
		return $reponse;
	}
	if ( isset( $reponse->get_headers()['X-Cache-WPM'] ) ) {
		return $reponse; // réponse déjà servie par le cache
	}

	$reponse->header( 'Cache-Control', 'public, max-age=60, s-maxage=600' );

	set_transient(
		wpm_cache_rest_cle( $requete ),
		array(
			'donnees'  => $reponse->get_data(),
			'statut'   => $reponse->get_status(),
			'en_tetes' => $reponse->get_headers(),
			'liens'    => $reponse->get_links(),
		),
		10 * MINUTE_IN_SECONDS
	);
	$reponse->header( 'X-Cache-WPM', 'MISS' );

	return $reponse;
}
```

Plusieurs choix méritent d'être expliqués. Seules les requêtes `GET` anonymes vers l'espace `/wp/v2/` sont mises en cache : une réponse propre à un utilisateur connecté ne doit jamais être servie à un autre. Les paramètres de la requête sont triés avant de fabriquer la clé, pour que `?per_page=10&page=2` et `?page=2&per_page=10` partagent la même entrée. Les en-têtes de la réponse (notamment `X-WP-Total` et `X-WP-TotalPages`, dont la pagination du front dépend) et les liens (`_links`) sont stockés avec les données ; sans eux, la réponse servie par le cache serait incomplète. L'en-tête `X-Cache-WPM` indique, à chaque appel, si la réponse vient du cache ou non.

La fonction `set_transient()` utilise le cache d'objets persistant (Redis ou Memcached) s'il est installé, et la table des options sinon. Dans les deux cas, le gain porte sur les requêtes SQL évitées, pas seulement sur le temps PHP.

## Les en-têtes HTTP qui pilotent le cache en amont

Le cache applicatif épargne la base de données, mais chaque requête traverse encore PHP. Les en-têtes HTTP permettent d'arrêter les requêtes plus tôt. Dans le code ci-dessus, `Cache-Control: public, max-age=60, s-maxage=600` dit deux choses : le navigateur peut réutiliser la réponse pendant soixante secondes, et un cache partagé (un CDN, un proxy inverse) pendant dix minutes. La directive `public` est nécessaire dès que la requête porte un en-tête `Authorization` ; sans authentification, elle reste une bonne pratique de lisibilité.

Deux compléments sont courants. Un en-tête `ETag`, calculé à partir du contenu, permet au client de revalider sa copie avec `If-None-Match` : si votre serveur ne traite pas les réponses 304, il renverra simplement un 200 complet, ce qui reste correct. La directive `stale-while-revalidate`, normalisée par la RFC 5861, autorise un cache à servir une copie légèrement périmée pendant qu'il la renouvelle ; vérifiez auprès de votre hébergeur qu'elle est bien prise en charge avant de vous y fier.

## Invalider au bon moment, sans tout vider

Un cache n'a de valeur que s'il se vide quand le contenu change. Plutôt que de supprimer une à une des entrées dont on ne connaît pas les clés, la technique la plus simple consiste à versionner la clé : on incrémente un numéro de version, et toutes les anciennes entrées deviennent inaccessibles. Elles expirent d'elles-mêmes à l'échéance de leur durée de vie.

```
add_action( 'transition_post_status', 'wpm_cache_rest_invalider', 10, 3 );
add_action( 'deleted_post', 'wpm_cache_rest_invalider_simple' );
add_action( 'edited_term', 'wpm_cache_rest_invalider_simple' );
add_action( 'delete_term', 'wpm_cache_rest_invalider_simple' );

function wpm_cache_rest_invalider( $nouveau, $ancien, $post ) {
	if ( 'publish' === $nouveau || 'publish' === $ancien ) {
		wpm_cache_rest_invalider_simple();
	}
}

function wpm_cache_rest_invalider_simple() {
	update_option( 'wpm_cache_rest_version', time(), false );
}
```

Le crochet `transition_post_status` est déclenché à chaque enregistrement, y compris quand l'état reste « publié » : il couvre donc la création, la modification, la dépublication et la mise à la corbeille. La condition évite de vider le cache pour un simple brouillon que personne ne peut voir. Les crochets sur les termes couvrent les catégories et étiquettes affichées dans les réponses.

## Vérifier que le cache fonctionne réellement

Un cache dont on ne mesure pas l'effet est une hypothèse. Deux appels successifs suffisent :

```
curl -s -o /dev/null -D - "https://exemple.fr/wp-json/wp/v2/posts?per_page=20" | grep -i "x-cache-wpm\|cache-control"
curl -s -o /dev/null -w "%{time_total}\n" "https://exemple.fr/wp-json/wp/v2/posts?per_page=20"
```

Le premier appel doit afficher `MISS`, le second `HIT`, avec un temps total nettement plus court. Publiez ensuite un article et relancez : le premier appel suivant doit de nouveau afficher `MISS`.

## Pièges à connaître

- **Le premier appel reste lent.** Un build statique qui démarre à froid paie toujours le coût du calcul. Préchauffez le cache en appelant vous-même les routes utiles juste après une publication.
- **Les réponses personnalisées n'ont rien à faire dans le cache.** Toute route dont le résultat dépend de l'utilisateur, d'un cookie ou d'un en-tête doit être exclue explicitement.
- **Les modifications de métadonnées passent parfois sous le radar.** Un champ mis à jour en dehors d'un enregistrement d'article (import, tâche planifiée) ne déclenche aucun des crochets ci-dessus ; appelez l'invalidation à la main dans ce cas.
- **Les en-têtes CORS interagissent avec le cache partagé.** Si votre front est sur un autre domaine, vérifiez que l'en-tête `Vary` est cohérent, sous peine de servir à un site l'autorisation accordée à un autre.
- **Le cache masque les erreurs.** Une réponse de travers mise en cache reste de travers pendant toute sa durée de vie : commencez par une durée courte.

> Un cache n'accélère pas un site, il le rend plus rapide à se tromper. Sans règle d'invalidation écrite noir sur blanc, il ne fait que retarder le jour où l'on s'en aperçoit.

## Conclusion

Trois niveaux travaillent ensemble : le cache applicatif épargne la base de données, les en-têtes `Cache-Control` évitent d'atteindre PHP, et un CDN absorbe le trafic lointain. L'extension WP REST Cache ou le code minimal ci-dessus règlent le premier niveau ; le deuxième tient en une ligne d'en-tête ; le troisième dépend de votre hébergeur. Quel que soit l'outil, posez dès le départ la règle d'invalidation et mesurez l'effet avec les deux commandes `curl` ci-dessus. C'est ce qui distingue un cache maîtrisé d'un cache subi.
