Les Service Workers, c’est l’une de ces technologies web qui font la différence entre un site ordinaire et une expérience vraiment solide — même sans connexion. Pourtant, dans l’écosystème WordPress, on a souvent le réflexe d’installer un plugin pour tout gérer à notre place. Et si, cette fois, on prenait le temps de comprendre et d’implémenter ça nous-mêmes, proprement, sans dépendance externe ?
Enregistrer et contrôler son Service Worker dans WordPress
Avant de plonger dans les stratégies de cache, il faut poser les bases correctement. L’enregistrement d’un Service Worker dans WordPress sans plugin dédié, c’est tout à fait faisable — mais quelques règles techniques sont à respecter scrupuleusement pour éviter les mauvaises surprises.
Où placer le fichier sw.js dans l’arborescence WordPress
C’est probablement le point le plus critique. Le fichier sw.js doit absolument être placé à la racine de votre domaine (ex: https://monsite.fr/sw.js), et pas ailleurs.
Pourquoi ? Parce que le scope d’un Service Worker est limité au répertoire depuis lequel il est servi. Si vous placez votre fichier dans /wp-content/themes/mon-theme/sw.js, votre SW ne pourra contrôler que les requêtes sous /wp-content/themes/mon-theme/ — ce qui est totalement inutile pour intercepter les pages de votre site.
Structure correcte :
/ ← racine du domaine
├── sw.js ← ici, et nulle part ailleurs
├── wp-admin/
├── wp-content/
└── index.php
Concrètement, déposez le fichier directement dans le dossier racine de WordPress via FTP, SSH, ou votre gestionnaire de fichiers d’hébergement. C’est simple, mais c’est non négociable.
Astuce : Si vous souhaitez quand même servir
sw.jsdepuis un sous-dossier (pour des raisons d’organisation), il est possible d’élargir le scope via l’en-tête HTTPService-Worker-Allowed: /. On y revient dans la section suivante.
Enregistrer le Service Worker avec le bon scope via wp_head
Une fois le fichier en place, il faut l’enregistrer côté client. L’approche la plus propre dans WordPress consiste à passer par functions.php pour injecter le script d’enregistrement.
Voici une méthode directe via wp_head :
// Dans functions.php
function mon_enregistrement_service_worker() {
?>
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/sw.js', { scope: '/' })
.then(function(registration) {
console.log('SW enregistré, scope :', registration.scope);
})
.catch(function(err) {
console.warn('Échec enregistrement SW :', err);
});
});
}
</script>
<?php
}
add_action('wp_head', 'mon_enregistrement_service_worker');
La détection 'serviceWorker' in navigator est indispensable : elle garantit que le code ne plantera pas sur les navigateurs qui ne supportent pas encore l’API (Safari ancienne version, certains navigateurs mobiles exotiques, etc.).
Et si vous voulez un scope élargi depuis un sous-dossier ? Il faut configurer votre serveur pour envoyer l’en-tête Service-Worker-Allowed: / avec le fichier sw.js. Sur Apache, ça donne dans votre .htaccess :
<Files "sw.js">
Header set Service-Worker-Allowed "/"
</Files>
Sur Nginx, dans votre configuration de bloc server :
location = /sw.js {
add_header Service-Worker-Allowed "/";
}
Gérer les conflits avec les plugins de cache existants
C’est là que ça se complique un peu. La plupart des plugins de cache populaires — WP Rocket, W3 Total Cache, LiteSpeed Cache — peuvent interférer avec votre sw.js de plusieurs façons : minification du script d’enregistrement qui casse le code, mise en cache agressive de sw.js lui-même côté serveur (ce qui empêche les mises à jour), ou encore exclusion du fichier de certaines optimisations.
Les règles d’exclusion à configurer :
- WP Rocket : dans Fichiers statiques > Exclure du cache navigateur, ajoutez
sw.js. Idem dans les options de minification JS, excluez le script d’enregistrement. - W3 Total Cache : désactivez la mise en cache des fichiers
.jsà la racine, ou ajoutez une règle d’exclusion explicite pour/sw.js. - LiteSpeed Cache : dans Cache > Exclure, ajoutez
/sw.jsdans les URI exclues. Vérifiez aussi que l’option « Cache des navigateurs » ne force pas un TTL trop long sur ce fichier.
L’idée est simple : sw.js doit toujours être servi « frais » par le serveur (avec un Cache-Control: no-cache idéalement), car c’est le navigateur qui gère ensuite son propre cycle de mise à jour du Service Worker. Si votre plugin de cache sert une version périmée de sw.js, vos utilisateurs ne bénéficieront jamais de vos mises à jour — et déboguer ça peut vite devenir un enfer.
Concevoir une stratégie de cache offline avancée
On a posé les bases dans la section précédente : le Service Worker est enregistré, il ne conflict pas avec WP Rocket ou LiteSpeed. Maintenant, on entre dans le vif du sujet. La vraie puissance d’un SW, c’est sa stratégie de cache. Et là, il faut choisir avec soin — parce qu’une mauvaise stratégie appliquée au mauvais type de ressource, c’est soit un site qui sert du contenu périmé, soit un site qui ne fonctionne tout simplement pas offline.
Cache First, Network First, Stale-While-Revalidate : choisir selon le type de ressource
Il existe plusieurs stratégies de cache. Workbox les abstrait très proprement, mais on peut tout à fait les implémenter nativement dans le Service Worker. Voici les trois principales à connaître pour WordPress :
Cache First : on cherche d’abord dans le cache, et si on ne trouve rien, on va chercher sur le réseau. C’est la stratégie idéale pour les assets versionnés — les fichiers CSS et JS de votre thème (
/wp-content/themes/mon-theme/style.css?ver=2.1), les Google Fonts, les images statiques. Ces ressources ne changent pas d’une visite à l’autre (ou presque), donc autant servir la version en cache directement.Network First : on tente d’abord le réseau, et si ça échoue (connexion coupée, timeout), on se rabat sur le cache. C’est ce qu’on utilisera pour les pages dynamiques WordPress, les résultats de recherche, ou toute page dont le contenu peut changer fréquemment.
Stale-While-Revalidate : on sert la version en cache immédiatement (pour la rapidité), mais on lance en parallèle une requête réseau pour mettre à jour le cache en arrière-plan. C’est parfait pour les articles de blog dont le contenu évolue peu. L’utilisateur a une réponse instantanée, et la prochaine visite aura une version fraîche.
Le bon réflexe : associer la stratégie au cycle de vie de la ressource. Assets figés → Cache First. Contenu dynamique critique → Network First. Contenu semi-statique → Stale-While-Revalidate.
Précaching des assets statiques WordPress (CSS, JS, fonts)
Le précaching, c’est l’action de mettre en cache un ensemble d’URLs au moment de l’installation du Service Worker (event install). On définit un tableau CACHE_URLS avec toutes les ressources que l’on veut avoir disponibles immédiatement, même offline.
Voici un exemple concret avec un système de versioning du cache — c’est crucial pour forcer le rechargement des assets quand on déploie une mise à jour :
// sw.js
const CACHE_NAME = 'devwp-cache-v2'; // Incrémentez cette version à chaque déploiement
const CACHE_URLS = [
'/', // Page d'accueil
'/offline/', // Page offline personnalisée (à créer dans WordPress)
'/wp-content/themes/mon-theme/style.css',
'/wp-content/themes/mon-theme/assets/js/main.js',
'/wp-content/themes/mon-theme/assets/fonts/inter-regular.woff2',
'/wp-content/themes/mon-theme/assets/fonts/inter-bold.woff2',
'/wp-content/themes/mon-theme/assets/img/logo.svg',
// Ajoutez ici vos autres assets critiques
];
// Event 'install' : on précache toutes les ressources définies dans CACHE_URLS
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
console.log('[SW] Précaching des assets statiques...');
return cache.addAll(CACHE_URLS);
}).then(() => {
// Force l'activation immédiate sans attendre la fermeture des onglets ouverts
return self.skipWaiting();
})
);
});
Un point important : si une seule URL du tableau échoue, tout le précaching échoue. Donc, vérifiez bien que vos URLs sont correctes avant de déployer. Et pensez à incrémenter CACHE_NAME à chaque déploiement — c’est la clé du versioning.
Runtime caching des pages et requêtes API REST WordPress
Le runtime caching, lui, fonctionne au moment où l’utilisateur navigue. On intercepte les requêtes fetch et on applique la stratégie adaptée selon l’URL demandée. C’est l’event fetch qui fait tout le travail ici.
Mais avant ça, l’event activate : il faut nettoyer les anciens caches pour éviter d’occuper inutilement l’espace de stockage avec des versions obsolètes.
// Event 'activate' : nettoyage des anciens caches
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME) // On garde uniquement le cache actuel
.map((name) => {
console.log('[SW] Suppression de l\'ancien cache :', name);
return caches.delete(name);
})
);
}).then(() => {
// Prend le contrôle de tous les onglets immédiatement
return self.clients.claim();
})
);
});
Ensuite, voici la logique de runtime caching dans l’event fetch, avec les stratégies Network First et Stale-While-Revalidate :
// Stratégie Network First (implémentation native)
function networkFirst(request, cacheName) {
return fetch(request)
.then((response) => {
// On clone la réponse car un body ne peut être lu qu'une seule fois
const responseClone = response.clone();
caches.open(cacheName).then((cache) => {
cache.put(request, responseClone);
});
return response;
})
.catch(() => {
// Réseau indisponible : on retourne la version en cache si elle existe
return caches.match(request).then((cached) => {
return cached || caches.match('/offline/');
});
});
}
// Stratégie Stale-While-Revalidate (implémentation native)
function staleWhileRevalidate(request, cacheName) {
return caches.open(cacheName).then((cache) => {
return cache.match(request).then((cachedResponse) => {
// On lance la requête réseau en parallèle pour mettre à jour le cache
const fetchPromise = fetch(request).then((networkResponse) => {
cache.put(request, networkResponse.clone());
return networkResponse;
});
// On retourne la version en cache si disponible, sinon on attend le réseau
return cachedResponse || fetchPromise;
});
});
}
// Stratégie Cache First (implémentation native)
function cacheFirst(request, cacheName) {
return caches.match(request).then((cached) => {
return cached || fetch(request).then((response) => {
return caches.open(cacheName).then((cache) => {
cache.put(request, response.clone());
return response;
});
});
});
}
Ces trois fonctions sont les briques de base. On les appelle depuis l’event fetch selon l’URL interceptée — on y vient juste après.
Gérer les routes exclues : admin, WooCommerce, formulaires
C’est probablement la partie la plus critique de toute la stratégie. Certaines routes ne doivent jamais passer par le cache. Si vous laissez le Service Worker intercepter les requêtes vers /wp-admin/, vous risquez des comportements imprévisibles — voire des problèmes de sécurité. Idem pour WooCommerce : le panier, le checkout, le compte client, tout ça doit toujours aller directement sur le réseau.
Voici l’event fetch complet avec les exclusions et la logique de dispatch :
// Listes des patterns à exclure absolument du cache
const EXCLUDED_ROUTES = [
/\/wp-admin\//, // Interface d'administration WordPress
/\/wp-login\.php/, // Page de connexion
/\/wp-json\//, // API REST WordPress (sauf si vous souhaitez la cacher spécifiquement)
/\/cart\//, // Panier WooCommerce
/\/checkout\//, // Page de paiement WooCommerce
/\/my-account\//, // Espace client WooCommerce
/\/wc-ajax=/, // Requêtes AJAX WooCommerce
/[?&]nocache/, // Paramètre nocache explicite
];
// Patterns pour les assets statiques (Cache First)
const STATIC_ASSETS_PATTERN = /\.(css|js|woff2?|ttf|eot|svg|png|jpg|jpeg|gif|webp|ico)(\?.*)?$/;
// Patterns pour les articles de blog (Stale-While-Revalidate)
// On suppose que les articles ont une URL du type /mon-article/ (hors pages d'accueil et recherche)
const BLOG_POST_PATTERN = /^\/[a-z0-9\-]+\/$/;
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// 1. On ne traite que les requêtes GET sur notre propre domaine
if (request.method !== 'GET' || url.origin !== self.location.origin) {
return; // Laisse passer sans interception
}
// 2. Exclusion des routes sensibles — TOUJOURS en premier
const isExcluded = EXCLUDED_ROUTES.some((pattern) => pattern.test(url.pathname + url.search));
if (isExcluded) {
console.log('[SW] Route exclue, pas de cache :', url.pathname);
return; // On ne fait rien, le navigateur gère la requête normalement
}
// 3. Assets statiques → Cache First
if (STATIC_ASSETS_PATTERN.test(url.pathname)) {
event.respondWith(cacheFirst(request, CACHE_NAME));
return;
}
// 4. Résultats de recherche et page d'accueil → Network First
if (url.pathname === '/' || url.search.includes('s=')) {
event.respondWith(networkFirst(request, CACHE_NAME));
return;
}
// 5. Articles de blog → Stale-While-Revalidate
if (BLOG_POST_PATTERN.test(url.pathname)) {
event.respondWith(staleWhileRevalidate(request, CACHE_NAME));
return;
}
// 6. Tout le reste → Network First par défaut (le plus sûr)
event.respondWith(networkFirst(request, CACHE_NAME));
});
Quelques précisions importantes sur ce code :
- Le check
request.method !== 'GET'est fondamental : on ne cache jamais les requêtes POST (formulaires, connexions, commandes WooCommerce). - L’ordre des conditions dans le
fetchest intentionnel : les exclusions passent toujours en premier, avant n’importe quelle logique de cache. - La regex
BLOG_POST_PATTERNest un exemple simple — adaptez-la à votre structure de permaliens WordPress (catégories, CPT, etc.).
Bon, c’est un peu de code à digérer d’un coup, j’en suis conscient. Mais la bonne nouvelle : une fois cette logique en place, vous avez une stratégie de cache offline robuste, sans dépendance externe, et parfaitement adaptée à WordPress.
Mettre à jour le Service Worker et déboguer dans WordPress
On a vu comment écrire les stratégies de cache, c’est bien. Mais le vrai sujet qui fait perdre des heures, c’est la mise à jour du SW et le débogage quand quelque chose ne se passe pas comme prévu. Soyons honnêtes : un Service Worker mal géré peut servir indéfiniment du contenu obsolète à vos utilisateurs. Voici comment éviter ça.
Stratégie de versioning et cycle de vie du SW (install → activate → fetch)
Le cycle de vie d’un Service Worker suit trois étapes clés : install, activate, puis fetch. Ce qu’il faut bien comprendre, c’est que le navigateur compare byte par byte le fichier sw.js à sa version précédente. Un seul caractère différent suffit à déclencher une mise à jour. C’est pourquoi versionner son cache est indispensable.
La bonne pratique consiste à définir une constante en haut du fichier :
const CACHE_VERSION = 'v3';
const CACHE_NAME = `mon-cache-${CACHE_VERSION}`;
Et dans l’event activate, on purge proprement tous les anciens caches :
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames
.filter(name => name !== CACHE_NAME)
.map(name => {
console.log('[SW] Suppression ancien cache :', name);
return caches.delete(name);
})
);
})
);
});
Maintenant, un piège classique : le nouveau SW s’installe mais attend que tous les onglets ouverts soient fermés avant de prendre la main. Pour forcer la prise en main immédiate, on utilise self.skipWaiting() dans l’event install et clients.claim() dans activate :
self.addEventListener('install', event => {
self.skipWaiting();
// ... reste du code d'install
});
self.addEventListener('activate', event => {
event.waitUntil(
clients.claim().then(() => {
// purge des anciens caches ici
})
);
});
Attention cependant : cette combinaison comporte un risque réel. Si un utilisateur a deux onglets ouverts en même temps et que votre cache change entre les deux, un onglet peut charger des ressources provenant de deux versions différentes de votre application. Pour un site vitrine classique, ce n’est généralement pas problématique. Pour une app WordPress complexe avec du JavaScript critique (WooCommerce par exemple), réfléchissez à deux fois avant d’utiliser skipWaiting() systématiquement.
Outils de débogage : Chrome DevTools, about:debugging, logs console
Le débogage d’un Service Worker, c’est un peu spécial. On ne peut pas simplement inspecter le DOM. Les outils sont là, mais encore faut-il savoir où regarder.
Dans Chrome DevTools, rendez-vous dans l’onglet Application > Service Workers. Vous y trouverez plusieurs options vraiment utiles :
- Update on reload : force la vérification du nouveau SW à chaque rechargement (à activer pendant le développement)
- Bypass for network : contourne totalement le SW, idéal pour comparer le comportement avec/sans cache
- Offline : simule une coupure réseau pour tester vos fallbacks
- Le bouton Update : force manuellement la mise à jour du SW sans attendre
L’onglet Cache Storage (juste en dessous) permet d’inspecter le contenu exact de vos caches, d’identifier ce qui est stocké et de supprimer des entrées manuellement.
Sous Firefox, l’équivalent se trouve dans about:debugging#/runtime/this-firefox. Vous pouvez inspecter les SW actifs et ouvrir leur console dédiée.
Pour les logs dans le SW lui-même, n’hésitez pas à être verbeux en développement :
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(cachedResponse => {
if (cachedResponse) {
console.log('[SW] Cache HIT :', event.request.url);
return cachedResponse;
}
console.log('[SW] Cache MISS, réseau :', event.request.url);
return fetch(event.request);
})
);
});
Ces logs apparaissent dans la console du SW (accessible via le lien « inspect » dans l’onglet Application), pas dans la console de la page principale. C’est un point qui surprend souvent.
Page offline de fallback : créer et servir une URL custom dans WordPress
C’est la partie la plus visible pour l’utilisateur final : quand il n’a pas de réseau et qu’il navigue vers une page non mise en cache, que voit-il ? Par défaut, le message d’erreur moche du navigateur. Avec un fallback bien pensé, il voit une page branded et utile.
Étape 1 : créer la page dans WordPress
Créez simplement une page avec le slug /offline/ depuis l’administration WordPress. Ajoutez-y un message clair, éventuellement des liens vers les pages déjà en cache. Publiez-la.
Étape 2 : passer l’URL au Service Worker via wp_localize_script
Le problème classique, c’est que l’URL de votre page offline peut changer selon la configuration du site (sous-dossier, multisite, etc.). On règle ça proprement côté PHP dans functions.php :
function enregistrer_service_worker() {
wp_register_script('mon-sw-register', get_template_directory_uri() . '/js/sw-register.js', [], null, true);
wp_localize_script('mon-sw-register', 'swConfig', [
'offlineUrl' => home_url('/offline/'),
]);
wp_enqueue_script('mon-sw-register');
}
add_action('wp_enqueue_scripts', 'enregistrer_service_worker');
Dans votre fichier sw-register.js, vous transmettez ensuite cette URL lors de l’enregistrement du SW via postMessage ou en la passant comme paramètre query string (ex: sw.js?offlineUrl=/offline/).
Étape 3 : précacher la page dans l’event install et servir le fallback dans fetch
const OFFLINE_URL = '/offline/'; // ou récupérée dynamiquement
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
return cache.addAll([
OFFLINE_URL,
// ... autres ressources à précacher
]);
})
);
self.skipWaiting();
});
self.addEventListener('fetch', event => {
// On ne gère que les requêtes de navigation (pages HTML)
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request).catch(() => {
console.log('[SW] Navigation offline, fallback vers :', OFFLINE_URL);
return caches.match(OFFLINE_URL);
})
);
return;
}
// Pour les autres ressources, votre stratégie habituelle...
});
Le point clé ici : on vérifie event.request.mode === 'navigate' pour ne cibler que les navigations vers des pages HTML. Inutile de servir la page offline pour une image ou un fichier CSS manquant. Et surtout, la page /offline/ doit absolument être dans le cache au moment de l’event fetch — d’où l’importance de la précacher dans install.
Un dernier piège à éviter : si votre plugin de cache WordPress génère une version statique de la page /offline/, assurez-vous qu’elle reste accessible même sans réseau. Vérifiez dans l’onglet Cache Storage que l’URL précachée correspond bien à ce que le navigateur reçoit (avec ou sans slash final, ça peut faire une différence).
