Background Workers
Service workers and background scripts for event-driven extension logic, lifecycle management, and persistent tasks
You are an expert in service workers and background scripts for building browser extensions. ## Key Points - Register all event listeners synchronously at the top level of the service worker; listeners registered inside async functions or timeouts will be missed after a restart. - Use `chrome.storage.session` for ephemeral state that should persist across service worker restarts but not browser restarts, and `chrome.storage.local` for durable state. - Prefer `chrome.alarms` over `setTimeout`/`setInterval` since timers are cancelled when the service worker terminates. - Storing state in module-level variables and expecting it to persist — the service worker can be terminated at any time and all in-memory state is lost. - Registering context menus on every service worker startup instead of only inside `onInstalled`, which leads to duplicate entries and errors.
skilldb get browser-extension-skills/Background WorkersFull skill: 218 linesBackground Workers — Browser Extension Development
You are an expert in service workers and background scripts for building browser extensions.
Overview
In Manifest V3, the background page is replaced by a service worker. Service workers are event-driven, have no DOM access, and are terminated when idle. This model reduces memory usage but requires careful state management and event registration.
Core Concepts
Registering the Service Worker
{
"background": {
"service_worker": "background.js",
"type": "module"
}
}
The "type": "module" field enables ES module import/export syntax in the service worker.
Service Worker Lifecycle
The service worker starts when an event it listens for fires and is terminated after roughly 30 seconds of inactivity (or 5 minutes maximum runtime in some cases).
// Event listeners MUST be registered synchronously at the top level.
// They cannot be registered inside async callbacks or setTimeout.
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === 'install') {
console.log('Extension installed for the first time');
initializeDefaults();
} else if (details.reason === 'update') {
console.log(`Updated from ${details.previousVersion}`);
migrateData();
}
});
chrome.runtime.onStartup.addListener(() => {
console.log('Browser started, service worker activated');
});
Alarms API for Periodic Work
Since service workers are short-lived, use chrome.alarms for recurring tasks:
// Set up an alarm on install
chrome.runtime.onInstalled.addListener(() => {
chrome.alarms.create('periodicSync', { periodInMinutes: 30 });
});
// Handle alarm events
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'periodicSync') {
fetchAndCacheData();
}
});
async function fetchAndCacheData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
await chrome.storage.local.set({ cachedData: data, lastSync: Date.now() });
} catch (err) {
console.error('Sync failed:', err);
}
}
OffscreenDocument for DOM-Dependent Tasks
Service workers have no DOM. When you need DOM APIs (e.g., parsing HTML, using <canvas>), create an offscreen document:
async function ensureOffscreenDocument() {
const existing = await chrome.offscreen.hasDocument();
if (!existing) {
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['DOM_PARSER'],
justification: 'Parse HTML content'
});
}
}
// offscreen.html includes offscreen.js which listens for messages
// and performs DOM-dependent work
Implementation Patterns
Persisting State Across Restarts
Since global variables are lost when the service worker terminates, persist important state:
// Bad: state is lost on termination
let count = 0;
// Good: persist to storage
async function incrementCounter() {
const { count = 0 } = await chrome.storage.session.get('count');
const newCount = count + 1;
await chrome.storage.session.set({ count: newCount });
return newCount;
}
chrome.storage.session (MV3) persists for the browser session but is cleared on restart. Use chrome.storage.local for data that should survive restarts.
Context Menus
Register context menu items inside onInstalled, not on every service worker start:
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
id: 'lookupSelection',
title: 'Look up "%s"',
contexts: ['selection']
});
chrome.contextMenus.create({
id: 'saveImage',
title: 'Save image via extension',
contexts: ['image']
});
});
chrome.contextMenus.onClicked.addListener((info, tab) => {
switch (info.menuItemId) {
case 'lookupSelection':
handleLookup(info.selectionText, tab);
break;
case 'saveImage':
handleSaveImage(info.srcUrl, tab);
break;
}
});
Keeping the Service Worker Alive (When Necessary)
For long-running operations, you can extend the lifetime, but use this sparingly:
// Use a periodic alarm to keep alive during a specific operation
async function performLongTask() {
// Create a keepalive alarm
chrome.alarms.create('keepalive', { periodInMinutes: 0.4 });
try {
await doExpensiveWork();
} finally {
chrome.alarms.clear('keepalive');
}
}
// Alternatively, for tasks under 5 minutes, the service worker
// stays alive as long as events are actively being processed.
Handling Extension Updates
chrome.runtime.onInstalled.addListener(async (details) => {
if (details.reason === 'update') {
const { settings } = await chrome.storage.local.get('settings');
if (settings && !settings.newFeatureFlag) {
settings.newFeatureFlag = true;
await chrome.storage.local.set({ settings });
}
}
});
Best Practices
- Register all event listeners synchronously at the top level of the service worker; listeners registered inside async functions or timeouts will be missed after a restart.
- Use
chrome.storage.sessionfor ephemeral state that should persist across service worker restarts but not browser restarts, andchrome.storage.localfor durable state. - Prefer
chrome.alarmsoversetTimeout/setIntervalsince timers are cancelled when the service worker terminates.
Core Philosophy
Service workers in Manifest V3 represent a fundamental shift from persistent background pages to an event-driven, ephemeral model. Embrace this constraint rather than fighting it. The service worker should be a thin event dispatcher that responds to browser events, delegates work to appropriate APIs, and persists any state it needs to survive termination. Treat every service worker activation as a potential fresh start.
Design your background logic around the principle of minimal residency. The service worker should do its work and get out of the way, freeing resources for the user's browsing. If you find yourself writing code to keep the service worker alive, that is a signal to reconsider your architecture. Long-running tasks belong behind alarms, offscreen documents, or native messaging — not in tricks that fight the platform's resource management.
Think of the service worker as the nervous system of your extension, not its brain. It routes signals between content scripts, popups, and external APIs, but it should not accumulate complex state or run heavy computation. Keep handlers focused, idempotent where possible, and always prepared for the possibility that the worker was just restarted.
Anti-Patterns
-
Treating global variables as persistent state — storing counters, caches, or configuration in module-level variables and assuming they survive across service worker restarts leads to subtle data loss and inconsistent behavior.
-
Polling with setTimeout/setInterval — using JavaScript timers for periodic work is unreliable because they are cancelled when the service worker terminates; always use
chrome.alarmsfor any recurring task. -
Registering listeners inside async callbacks — placing event listener registrations inside
setTimeout,fetch().then(), or any async path causes them to be missed when the service worker restarts, because Chrome only recognizes listeners registered synchronously at the top level. -
Keeping the service worker alive artificially — using keepalive alarms or port connections solely to prevent termination wastes system resources and violates the spirit of MV3; restructure the logic to work within the ephemeral model instead.
-
Performing DOM operations directly — attempting to use DOM APIs like
document.createElementorDOMParserin the service worker fails silently or throws; offscreen documents exist specifically for this purpose.
Common Pitfalls
- Storing state in module-level variables and expecting it to persist — the service worker can be terminated at any time and all in-memory state is lost.
- Registering context menus on every service worker startup instead of only inside
onInstalled, which leads to duplicate entries and errors.
Install this skill directly: skilldb add browser-extension-skills
Related Skills
Content Scripts
Content scripts for DOM manipulation, page interaction, and isolated world execution in browser extensions
Cross-Browser
Cross-browser extension compatibility across Chrome, Firefox, and Safari with API normalization and build strategies
Extension Publishing
Publishing browser extensions to Chrome Web Store and Firefox Add-ons including review processes and automation
Manifest V3
Chrome Manifest V3 fundamentals including configuration, permissions, and migration from V2
Messaging
Message passing between extension components including content scripts, service workers, popups, and external pages
Popup UI
Building popup and options page UIs for browser extensions with responsive layouts and framework integration