Service Worker Background Scripts in Action
In the Manifest V2 era, extensions had something called background.html or background.js, which was an invisible webpage that persisted in the background consuming memory as long as your extension was enabled. This led to severe browser slowdowns when users installed dozens of extensions.
In Manifest V3, Google replaced persistent Background Pages with Service Workers.
1. What is a Service Worker?
Think of a Service Worker as an "on-demand, disposable" worker:
- When an event occurs (e.g., a user clicks the extension icon or a timer triggers), the browser "wakes up" the Service Worker to execute tasks.
- Once the task is completed or after being idle for a few seconds, the browser "terminates" it to free up memory.
[!CAUTION] Biggest Pitfall: Global variables disappear! Since Service Workers can be randomly terminated and restarted, you must never rely on global variables to store important states.
// ❌ Never do this! The variable `count` will reset to zero when the worker is terminated. let count = 0; chrome.action.onClicked.addListener(() => { count++; });The correct approach is to use the
chrome.storageAPI to save states to disk.
2. Registering a Service Worker
To use a Service Worker, we must first register it in manifest.json:
{
"manifest_version": 3,
"name": "My Service Worker App",
"version": "1.0",
"background": {
"service_worker": "background.js",
"type": "module"
},
"permissions": [
"storage",
"alarms"
]
}
Note the addition of type: "module", which allows us to use ES6 import / export syntax in background.js, greatly aiding code management in large projects.
3. Practical Example: Building a Pomodoro Timer (Alarms API)
Since Service Workers cannot use standard setTimeout or setInterval (timers stop working if the worker goes idle), we must use Chrome's proprietary chrome.alarms API.
Let's implement a simple Pomodoro timer logic in background.js:
// Listen for extension installation or update events
chrome.runtime.onInstalled.addListener(() => {
console.log("Extension installed, initializing settings...");
// Default Pomodoro timer duration: 25 minutes
chrome.storage.local.set({ timerDuration: 25 });
});
// Listen for messages from Popup or Content Script
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'startTimer') {
// Create an Alarm
chrome.alarms.create('pomodoroTimer', { delayInMinutes: request.minutes });
console.log(`Timer set for ${request.minutes} minutes!`);
sendResponse({ status: "success" });
}
});
// Listen for Alarm triggers
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'pomodoroTimer') {
// Time's up! Trigger a notification
console.log("Time's up! Take a break!");
// If you have the "notifications" permission, show a system notification here
chrome.notifications.create({
type: "basic",
iconUrl: "icons/icon128.png",
title: "Pomodoro Complete",
message: "Great job! Time to stretch your legs!"
});
}
});
4. Lifecycle and Event-Driven Design
The core philosophy of developing V3 Service Workers is "event-driven."
Your background.js should be entirely wrapped in chrome.xxx.addListener() calls.
Common event listeners include:
chrome.runtime.onInstalled: Executes when the extension is first installed (ideal for initializing databases or displaying onboarding pages).chrome.tabs.onUpdated: Triggers when users switch tabs or a webpage finishes loading (useful for enabling features on specific URLs).chrome.action.onClicked: Triggers when users click the extension icon (note: if your manifest has adefault_popupset, this event won't trigger because the click is intercepted by the popup).
5. Fetch API and Asynchronous Handling
Service Workers are also the best place to communicate with backend databases (like Supabase) or external APIs, as they face fewer cross-origin (CORS) restrictions (provided you declare the permissions in the manifest).
// Calling an external API in background.js
async function fetchUserData(userId) {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
const data = await response.json();
// Store the fetched data in storage for popup.js to read
await chrome.storage.local.set({ userData: data });
return data;
} catch (error) {
console.error("API request failed:", error);
}
}
Summary
Service Workers are the soul of Manifest V3. By adapting to their ephemeral nature, leveraging chrome.storage for state persistence, and embracing event-driven programming, you can build ultra-smooth, non-blocking extensions.
In the next chapter, we'll introduce the "hands and feet" of extensions—Content Scripts—and teach you how to directly manipulate the web pages users are viewing!
Service Worker Lifecycle
Installing → Installed → Activating → Activated → Idle → Terminated
| | | | | |
| | | | | (≈30s after
| | | | | last event)
▼ ▼ ▼ ▼ ▼
'install' 'activated' 'activate' 'message' (terminated)
| State | Description | Duration | |-------|-------------|----------| | Installing | First run after registration | ~1-5s | | Installed | Ready but previous SW still running | Until activate | | Activating | Taking over from old SW | ~1-2s | | Activated | Fully operational, handling events | ~5 min (idle) | | Terminated | Killed by browser to save memory | Until next event |
Event-Driven Programming
Since service workers are ephemeral, you must use event-driven patterns:
// ❌ WRONG: setTimeout-based polling (won't work after SW terminates)
setInterval(() => fetchData(), 60000);
// ✅ CORRECT: Use chrome.alarms API
chrome.alarms.create('fetch-data', { periodInMinutes: 1 });
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'fetch-data') {
fetchData();
}
});
chrome.alarms API
| Method | Description |
|--------|-------------|
| chrome.alarms.create(name, options) | Create a recurring alarm |
| chrome.alarms.get(name) | Get alarm details |
| chrome.alarms.getAll() | List all alarms |
| chrome.alarms.clear(name) | Remove an alarm |
| chrome.alarms.clearAll() | Remove all alarms |
| chrome.alarms.onAlarm | Fired when alarm triggers |
Alarm Options
// Delay in minutes (relative)
chrome.alarms.create('reminder', { delayInMinutes: 5 });
// Recurring (every N minutes)
chrome.alarms.create('sync', { periodInMinutes: 15 });
// Specific time (absolute)
chrome.alarms.create('daily-report', {
when: Date.now() + 86400000, // 24 hours from now
periodInMinutes: 1440 // Repeat every 24 hours
});
Keeping State Across Terminations
// Service worker terminates → all in-memory state is lost
// Solution: use chrome.storage
// Save state before termination
chrome.runtime.onSuspend?.addListener(async () => {
await chrome.storage.local.set({
lastState: {
count: currentCount,
lastSync: Date.now(),
pendingItems: queue
}
});
});
// Restore state on wake
chrome.runtime.onStartup.addListener(async () => {
const { lastState } = await chrome.storage.local.get('lastState');
if (lastState) {
currentCount = lastState.count || 0;
queue = lastState.pendingItems || [];
console.log(`Restored state: ${currentCount} items`);
}
});
Debugging Service Workers
| Tool | How to Access |
|------|--------------|
| Chrome DevTools | chrome://extensions → Inspect views: Service Worker |
| Console logging | console.log() appears in DevTools console |
| Breakpoints | Set in DevTools Sources tab |
| Network tab | See fetch requests from the service worker |
| Application tab | Storage, caches, service worker status |
Best Practices
| Practice | Reason |
|----------|--------|
| Use chrome.alarms instead of setInterval | SW terminates between events |
| Save state to chrome.storage | SW is ephemeral, storage persists |
| Minimize global variables | Reset when SW restarts — use storage instead |
| Handle chrome.runtime.onSuspend | Save critical state before termination |
| Test with "Update" button in DevTools | Force service worker restart |
| Add error handling everywhere | Unhandled errors kill the SW silently |
| Use self.addEventListener | Standard Service Worker API |
| Register listeners at top level | Must be synchronous (not inside async functions) |
Common Pitfalls
| Pitfall | Solution |
|---------|----------|
| Trying to access DOM | Service workers can't access DOM — use messaging |
| Using window or document | Not available — use self or chrome APIs |
| Async listener registration | Listeners must be registered synchronously |
| Heavy computation in SW | Delegate to a dedicated worker or offscreen document |
| Expected persistent state | Store in chrome.storage, not in variables |
| fetch() from the wrong origin | Use mode: 'cors' for cross-origin requests |
Summary
Service workers are event-driven, ephemeral background scripts in Manifest V3. Use chrome.alarms for scheduling, chrome.storage for persistence, and event-driven patterns for reliability. Always register listeners synchronously at the top level.
Key takeaways:
- Service workers terminate ~30s after last event — they are not persistent
- Use
chrome.alarmsinstead of timers - Save critical state to
chrome.storagebefore termination - Register listeners synchronously at the module top level
- Debug via
chrome://extensions→ Inspect: Service Worker - No DOM access — use messaging to communicate with content scripts
- Use
self.addEventListenernotwindow.addEventListener - Handle
onSuspendfor graceful state saving
What's Next: Content Scripts
The next chapter covers Content Scripts — injecting JavaScript into web pages to interact with the DOM.