Dossier technique
Manifest V3 et service workers : ce que ça change pour les extensions VPN
Manifest V3 ne change pas seulement le format du manifeste. Il change le modèle d’exécution. Une extension VPN n’est plus adossée à une page d’arrière-plan persistante ; elle s’appuie sur un service worker événementiel, chargé à la demande puis arrêté lorsqu’il devient inactif. Cette bascule a des conséquences directes sur l’état, le cycle de vie, les timers, l’accès au DOM, la structure du code et la fiabilité opérationnelle.
Manifest V3 a déplacé le contexte d’arrière-plan des extensions Chrome vers un service worker. Ce point est souvent résumé de manière trop vague, comme s’il s’agissait d’un simple renommage. En réalité, le changement est plus dur : une extension ne peut plus compter sur un contexte persistant vivant en permanence. Elle doit fonctionner dans un modèle événementiel, éphémère, soumis à des arrêts automatiques et à une réactivation à la demande.
Pour une extension VPN, ce changement est structurant. Une partie de sa logique repose sur la continuité apparente de l’état, sur la disponibilité rapide de ses réglages, sur la cohérence de sa politique réseau et sur la capacité à réagir à des événements sans perdre le fil entre deux exécutions. Dès que le service worker peut être arrêté, tout code qui dépend d’un état global implicite ou d’un timer fragile devient suspect.
Le manifeste ne décrit plus une page persistante mais un point d’entrée unique
En Manifest V3, le champ background.scripts disparaît au profit de
background.service_worker. Le service worker est déclaré par une chaîne unique, pas
par un tableau de scripts. Si l’extension utilise des modules ES, le champ
type peut être défini à module.
{
"manifest_version": 3,
"background": {
"service_worker": "service-worker.js",
"type": "module"
}
}
Ce détail de syntaxe signale en réalité un changement bien plus large. Le cœur d’exécution n’est plus une page d’arrière-plan durable, mais un worker qui répond à des événements, puis peut disparaître. Ce n’est pas seulement une autre manière de charger du JavaScript ; c’est un autre contrat d’exécution.
Le service worker devient le gestionnaire central d’événements
Un service worker d’extension est le point central de traitement des événements de l’extension. Il répond aux événements standard des service workers, mais aussi aux événements des namespaces d’extension. Il n’est donc pas seulement un mécanisme réseau ; il sert de pivot pour la logique de cycle de vie, les événements runtime, les actions utilisateur, certains changements d’état et divers déclencheurs propres aux extensions.
Ce modèle a une conséquence immédiate pour une extension VPN : il faut penser le code comme une série de réactions atomiques à des événements, pas comme un processus continu qui resterait toujours vivant. Une politique de routage, un cache d’options, une synchronisation de réglages ou un signal d’état doivent être reconstruits ou rechargés proprement à chaque réveil.
Le cycle de vie est éphémère par conception
Chrome termine normalement un service worker après une période d’inactivité, lorsqu’un traitement
unique dépasse certaines limites de durée, ou lorsqu’une réponse fetch() tarde trop
à arriver. La documentation Chrome indique notamment une terminaison après environ 30 secondes
d’inactivité, une limite de l’ordre de 5 minutes pour un traitement unique, et un plafond d’environ
30 secondes pour l’arrivée d’une réponse fetch().
Cette logique ne doit pas être lue comme une gêne occasionnelle, mais comme une contrainte de conception. Une extension VPN mal architecturée peut donner l’illusion d’un état continu, puis perdre des données temporaires, manquer un comportement attendu ou réapparaître avec une mémoire partielle de son contexte précédent.
Le vrai sujet n’est donc pas “le worker peut s’éteindre”, mais “l’extension a-t-elle été conçue comme si cet arrêt était normal”. C’est sur ce point que se séparent les implémentations solides et celles qui continuent de penser en logique de page persistante.
Les variables globales ne sont plus un support de vérité
Dès lors que le service worker peut être arrêté puis relancé, les variables globales cessent d’être un stockage fiable. Chrome le dit explicitement : les valeurs portées par des variables globales sont perdues lorsque le service worker est arrêté. Une extension VPN qui garderait dans des globals l’état d’une connexion, un cache de politique ou un snapshot de configuration sans stratégie de persistance s’expose à un comportement incohérent.
Cela ne signifie pas qu’aucun cache mémoire n’est permis. Cela signifie qu’un cache mémoire ne peut plus être considéré comme la source de vérité. Le modèle correct consiste à persister l’état utile dans un stockage adapté, puis à recharger ce qui est nécessaire au moment où l’événement est traité.
const stateCache = { ready: false, profile: null };
const initState = chrome.storage.local.get(["profile"]).then((items) => {
stateCache.profile = items.profile || null;
stateCache.ready = true;
});
chrome.action.onClicked.addListener(async () => {
await initState;
// Logique dépendante de l’état rechargé.
});
chrome.storage, IndexedDB et CacheStorage remplacent les réflexes de page persistante
Le Web Storage API accessible via window.localStorage n’est pas disponible dans un
service worker d’extension. Chrome recommande de s’appuyer plutôt sur chrome.storage
pour la plupart des usages, et cite aussi IndexedDB ou CacheStorage selon les besoins.
Pour une extension VPN, cela impose une hiérarchie propre :
chrome.storagepour les options, états persistants et préférences structurées ;- IndexedDB pour des besoins plus complexes ou volumineux ;
- CacheStorage seulement si l’extension traite réellement des couples requête/réponse.
Le point critique n’est pas seulement le choix du support, mais le timing. Comme le worker ne reste pas chargé en permanence, une extension peut devoir précharger de manière asynchrone son état depuis le stockage avant d’exécuter un gestionnaire d’événement.
Les listeners doivent être enregistrés de façon synchrone
C’est l’un des pièges les plus sous-estimés de MV3. Si un listener est enregistré de manière asynchrone, par exemple à l’intérieur d’une promesse, d’un callback ou d’un chemin d’exécution tardif, Chrome n’offre pas de garantie fiable sur sa présence au moment où l’événement se déclenche. Dans un monde persistant, ce genre d’erreur pouvait survivre sans se voir. Dans un monde réinitialisable, elle devient structurelle.
Le bon principe est simple : les listeners critiques doivent être déclarés au niveau supérieur du service worker, immédiatement à l’évaluation du script, pas plus tard.
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === "install") {
chrome.contextMenus.create({
id: "vpn-status",
title: "VPN status",
contexts: ["action"]
});
}
});
chrome.action.onClicked.addListener(async () => {
// Traitement principal.
});
Une extension VPN qui enregistre sa logique réseau après un chargement d’options mal placé ou après une initialisation tardive peut simplement manquer l’événement qu’elle croyait attendre.
Les timers ne sont pas fiables comme mécanisme central de continuité
Chrome avertit explicitement que l’arrêt du service worker peut interrompre des timers avant leur
exécution. Là encore, le point n’est pas anecdotique. Toute logique critique reposant sur
setTimeout() ou setInterval() comme si le contexte allait rester vivant
devient fragile.
La recommandation est de remplacer les timers durables par chrome.alarms lorsque la
logique métier doit survivre à la dormance du worker. Pour une extension VPN, cela concerne par
exemple des vérifications périodiques, des rafraîchissements d’état, des nettoyages différés ou
des routines de maintenance.
chrome.alarms.create("refreshPolicy", { periodInMinutes: 1 });
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name !== "refreshPolicy") return;
// Rafraîchissement d’état ou revalidation d’une politique.
});
Un service worker n’a pas accès au DOM
Le service worker ne dispose ni du DOM ni de l’interface window. C’est un point de
rupture clair avec certaines habitudes anciennes de développement d’extensions. Toute logique
qui dépend d’une page cachée, d’un accès direct au document ou d’opérations DOM en arrière-plan
doit être déplacée ailleurs.
Chrome a précisément introduit l’Offscreen API pour répondre à certains besoins de DOM en arrière-plan. Un document offscreen peut fournir un vrai contexte de page sans ouvrir d’onglet visible, mais il ne doit pas être confondu avec une résurrection déguisée de la page persistante. Il reste un outil ciblé, avec sa propre permission et ses propres contraintes.
{
"permissions": ["offscreen"]
}
await chrome.offscreen.createDocument({
url: "offscreen.html",
reasons: ["CLIPBOARD"],
justification: "Background DOM task"
});
Les documents offscreen ne remplacent pas un mauvais design
L’Offscreen API existe pour traiter certains cas qui nécessitent un accès DOM en tâche de fond.
Chrome précise que le document offscreen est une page HTML statique empaquetée avec l’extension,
qu’il ne peut pas recevoir le focus, et que l’API runtime est la seule API
d’extension disponible directement dans ce contexte.
Autrement dit, le document offscreen est un outil de délégation, pas un prétexte pour recréer une architecture centrée sur un contexte toujours vivant. Si une extension VPN dépend d’un document offscreen pour tout son comportement normal, elle signale souvent une difficulté d’adaptation à MV3 plutôt qu’une bonne maîtrise du modèle.
XMLHttpRequest doit céder la place à fetch()
Chrome indique explicitement que les service workers d’extension ne sont pas compatibles avec
XMLHttpRequest() comme l’étaient d’anciens contextes d’arrière-plan. Le modèle
attendu passe par fetch().
Là encore, ce n’est pas un simple changement de style. Pour une extension VPN, toute récupération
de configuration distante autorisée, de métadonnées, de listes d’exceptions, de profils ou
d’états doit être pensée en cohérence avec les délais, les arrêts possibles du worker et les
contraintes de fetch().
const response = await fetch("https://example.invalid/policy.json");
const policy = await response.json();
MV3 durcit aussi la logique de sécurité autour du code
La migration vers MV3 ne concerne pas seulement le cycle de vie du worker. Chrome rappelle aussi que le code arbitraire hébergé à distance reste interdit dans les extensions. Cette contrainte force une séparation plus nette entre configuration interprétable et chemins d’exécution réels.
Pour une extension VPN, ce point est important. Une politique distante, une liste d’URLs, un ensemble de paramètres ou un profil de routage peuvent être récupérés et interprétés, mais l’extension ne doit pas se comporter comme un chargeur de code externe. Plus l’éditeur mélange configuration et logique d’exécution, plus la lecture sécurité devient défavorable.
L’initialisation doit être pensée comme un redémarrage fréquent
Une extension MV3 sérieuse doit supposer qu’elle redémarre souvent. Cela implique une
initialisation rapide, déterministe et résistante. Chrome documente d’ailleurs une approche de
préchargement asynchrone depuis chrome.storage avant traitement d’événements
utilisateur, précisément parce que le worker n’est pas toujours déjà en mémoire.
Pour une extension VPN, cela veut dire que les points suivants doivent être testés explicitement :
- reconstruction correcte de l’état après dormance ;
- réenregistrement fiable des listeners ;
- disponibilité immédiate des politiques réseau nécessaires ;
- absence de dépendance cachée à un global perdu ;
- gestion propre des erreurs d’initialisation asynchrone.
Pourquoi MV3 compte particulièrement pour une extension VPN
Une extension VPN ne se limite pas à changer un thème ou injecter une interface. Elle intervient sur des paramètres réseau, des politiques de confidentialité, des exceptions, des profils et des comportements dont l’utilisateur attend souvent une continuité implicite. C’est précisément le type de produit pour lequel une mauvaise adaptation au modèle événementiel se voit très vite.
Si l’éditeur ne maîtrise pas MV3, plusieurs symptômes apparaissent : état incohérent après réveil, politique de routage mal réappliquée, divergences entre interface et configuration effective, comportements différents selon l’ordre des événements, ou dépendance excessive à des caches non persistés.
Le passage à MV3 ne doit donc pas être lu comme une contrainte cosmétique imposée par Google, mais comme un test de maturité d’architecture.
Ce qu’il faut auditer concrètement
- Le service worker est-il déclaré proprement dans
background.service_worker? - Les listeners critiques sont-ils enregistrés de manière synchrone au chargement ?
- L’extension dépend-elle encore d’un état global implicitement persistant ?
- Les données utiles sont-elles rechargées depuis
chrome.storageou un support adapté ? - Des timers fragiles ont-ils été remplacés par
chrome.alarmsquand c’est nécessaire ? - Le besoin d’accès DOM est-il délégué proprement vers un document offscreen plutôt que bricolé ?
- Les flux distants passent-ils par
fetch()et une logique tolérante à la dormance ? - L’extension semble-t-elle avoir été pensée pour redémarrer souvent sans perdre sa cohérence ?
La suite logique est de croiser cette lecture avec la politique de routage navigateur, les mécanismes de traitement des requêtes et, en synthèse, avec la méthode d’audit complète.
Conclusion
Manifest V3 impose aux extensions VPN un changement réel de discipline technique. Le service worker n’est pas une page persistante rebaptisée : c’est un contexte événementiel, sans DOM, sans garantie de continuité mémoire, avec une logique de réveil, d’arrêt et de rechargement. Une extension qui tient correctement sous MV3 prouve surtout qu’elle a été pensée pour perdre son contexte sans perdre sa cohérence.