Chrome Extension Service Worker Tips — Manifest V3 Guide
4 min readMV3 Service Worker Tips
Practical tips for building robust Chrome Extension service workers using Manifest V3.
1. Register Listeners at Top Level (Mandatory) {#1-register-listeners-at-top-level-mandatory}
Service workers terminate unexpectedly. All event listeners must be at the top level.
// ✅ CORRECT
chrome.runtime.onInstalled.addListener(() => {});
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === 'FETCH') { fetchData().then(sendResponse); return true; }
});
// ❌ WRONG - Won't register
async function init() { chrome.runtime.onInstalled.addListener(() => {}); }
2. No Global State — Use @theluckystrike/webext-storage {#2-no-global-state-use-theluckystrikewebext-storage}
Global variables are lost on restart. Use chrome.storage for persistence.
import { Storage } from '@theluckystrike/webext-storage';
const storage = new Storage();
await storage.set('settings', { theme: 'dark' });
const { theme } = await storage.get('settings', { theme: 'light' });
storage.onChanged.addListener((changes) => console.log('Changed:', changes));
3. No setInterval — Use chrome.alarms {#3-no-setinterval-use-chromealarms}
setInterval/setTimeout don’t work reliably. Use chrome.alarms instead.
chrome.alarms.create('sync', { periodInMinutes: 15 });
chrome.alarms.onAlarm.addListener((alarm) => { if (alarm.name === 'sync') doSync(); });
4. Handle Restart in onStartup {#4-handle-restart-in-onstartup}
Re-initialize state when the service worker starts.
chrome.runtime.onStartup.addListener(async () => {
chrome.alarms.create('sync', { periodInMinutes: 15 });
const state = await storage.get('appState');
});
5. Use Offscreen Docs for DOM APIs {#5-use-offscreen-docs-for-dom-apis}
Service workers cannot access DOM. Use offscreen documents.
async function parseHTML(html) {
// Check if an offscreen document already exists by querying contexts
const contexts = await chrome.runtime.getContexts({
contextTypes: ['OFFSCREEN_DOCUMENT']
});
if (contexts.length === 0) {
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: [chrome.offscreen.Reason.DOM_PARSER],
justification: 'Parse HTML'
});
}
}
6. No window/document/localStorage/XMLHttpRequest {#6-no-windowdocumentlocalstoragexmlhttprequest}
These APIs are unavailable in service workers.
// ❌ localStorage.getItem() // Error!
// ❌ window.document // Error!
// ❌ XMLHttpRequest // Error!
// ✅ Use storage + fetch instead
7. ES Modules: “type”: “module” in manifest {#7-es-modules-type-module-in-manifest}
Enable ES modules in manifest.json.
{ "background": { "service_worker": "sw.js", "type": "module" } }
import { Storage } from './utils/storage.js';
8. Debug at chrome://extensions {#8-debug-at-chromeextensions}
Open chrome://extensions, enable Developer mode, click your extension’s “service worker” link.
const DEBUG = true;
function log(...args) { if (DEBUG) console.log('[SW]', ...args); }
9. @theluckystrike/webext-messaging for Typed Messages {#9-theluckystrikewebext-messaging-for-typed-messages}
Use @theluckystrike/webext-messaging for reliable message passing.
import { MessageChannel } from '@theluckystrike/webext-messaging';
const channel = new MessageChannel('my-app');
await channel.send('content-script', { type: 'UPDATE', data: {} });
channel.onMessage.addListener((msg) => console.log('Received:', msg));
Summary
| Tip | Action | |—–|——–| | Top-level listeners | Register before async | | No globals | Use storage | | No setInterval | Use alarms | | Handle onStartup | Re-initialize | | DOM APIs | Use offscreen docs | | No localStorage/XMLHttpRequest | Use storage/fetch | | ES modules | Add “type”: “module” | | Debug | Use chrome://extensions | | Messaging | Use webext-messaging | -e —
Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.