Overview
Learn how extensions initialize, manage state, and respond to browser lifecycle events. This lesson ensures TabNotes handles installation, updates, and browser restarts gracefully.
By the end, you’ll understand service worker lifecycle, initial setup flows, and state persistence strategies.
What You’ll Learn
- Handle extension installation with
runtime.onInstalled - Detect extension updates vs fresh installs
- Implement first-run welcome experiences
- Understand service worker termination and restart
- Persist state across service worker restarts
- Initialize default settings on install
- Debug lifecycle events
Extension Installation Event
Detecting First Install
The runtime.onInstalled event fires when:
- Extension is freshly installed
- Extension is updated to new version
- Chrome is updated
// entrypoints/background.ts
export default defineBackground(() => {
browser.runtime.onInstalled.addListener((details) => {
console.log('Extension installed:', details);
});
});
details Object:
{
reason: 'install' | 'update' | 'chrome_update',
previousVersion: '1.0.0' // Only present if reason is 'update'
}
First-Run Setup
Initialize default settings on fresh install:
browser.runtime.onInstalled.addListener(async (details) => {
if (details.reason === 'install') {
// Set default settings
await storage.setItem('sync:settings', {
theme: 'light',
autoSave: true
});
// Open welcome page
await browser.tabs.create({
url: browser.runtime.getURL('welcome.html')
});
}
});
Service Worker Lifecycle
Start → Execute → Terminate
Service workers don’t run continuously:
- Event Triggers → Worker starts
- Handler Executes → Code runs
- Idle Period (~30s) → Worker terminates
Important: Don’t rely on global variables persisting:
// ❌ This state is lost when worker terminates
let noteCount = 0;
browser.tabs.onActivated.addListener(() => {
noteCount++; // Resets to 0 on next worker start
});
// ✅ Use storage for persistent state
browser.tabs.onActivated.addListener(async () => {
const stats = await storage.getItem('local:stats') || { count: 0 };
stats.count++;
await storage.setItem('local:stats', stats);
});
Waking the Service Worker
Workers automatically restart when:
- Tab changes (
tabs.onActivated) - URLs update (
tabs.onUpdated) - Messages arrive (
runtime.onMessage) - Alarms fire (
alarms.onAlarm)
Handling Extension Updates
Migrating Data
When updating to new versions, migrate old data structures:
browser.runtime.onInstalled.addListener(async (details) => {
if (details.reason === 'update') {
const oldVersion = details.previousVersion;
if (oldVersion && oldVersion < '2.0.0') {
// Migrate old data format to new format
const oldNotes = await storage.getItem('local:notes');
const newNotes = transformOldToNew(oldNotes);
await storage.setItem('sync:notesStorage', newNotes);
}
}
});
Showing Update Messages
Notify users about new features:
browser.runtime.onInstalled.addListener(async (details) => {
if (details.reason === 'update') {
await browser.tabs.create({
url: browser.runtime.getURL('whats-new.html')
});
}
});
State Management Strategies
What to Store Where
chrome.storage.sync (100KB limit):
- User preferences
- Notes (if under limit)
- Settings
chrome.storage.local (10MB limit):
- Cached data
- Analytics
- Large datasets
In-Memory (Lost on worker restart):
- Nothing important
- Only temporary calculations
Example: TabNotes State
// Persistent state (survives restarts)
await storage.setItem('sync:notesStorage', notes);
await storage.setItem('sync:settings', settings);
// Temporary state (lost on restart - acceptable)
let lastActiveTab = null; // Re-fetched on next event
Background Script Best Practices
Initialize on Every Start
Don’t assume worker state persists:
export default defineBackground(() => {
// ✅ Re-register listeners every time worker starts
browser.tabs.onActivated.addListener(handleTabChange);
browser.tabs.onUpdated.addListener(handleTabUpdate);
// ❌ Don't do one-time setup assuming persistence
// let initialized = false;
// if (!initialized) { ... }
});
Avoid Long-Running Operations
// ❌ Blocks worker from terminating
setInterval(() => {
checkForUpdates();
}, 60000);
// ✅ Use alarms API for scheduled tasks
browser.alarms.create('checkUpdates', { periodInMinutes: 1 });
browser.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'checkUpdates') {
checkForUpdates();
}
});
Creating Welcome Pages
Basic Welcome Experience
<!-- entrypoints/welcome.html -->
<!DOCTYPE html>
<html>
<head>
<title>Welcome to TabNotes</title>
</head>
<body>
<h1>Welcome to TabNotes!</h1>
<p>Start taking notes on any webpage:</p>
<ol>
<li>Click the extension icon</li>
<li>Type your note</li>
<li>Note saves automatically</li>
</ol>
<button id="get-started">Get Started</button>
<script src="./welcome.ts"></script>
</body>
</html>
// entrypoints/welcome.ts
document.getElementById('get-started')?.addEventListener('click', () => {
window.close();
});
Triggering Welcome Page
// entrypoints/background.ts
browser.runtime.onInstalled.addListener(async (details) => {
if (details.reason === 'install') {
await browser.tabs.create({
url: browser.runtime.getURL('welcome.html')
});
}
});
Debugging Lifecycle Events
Logging Lifecycle
export default defineBackground(() => {
console.log('Service worker started');
browser.runtime.onInstalled.addListener((details) => {
console.log('onInstalled:', details);
});
browser.runtime.onStartup.addListener(() => {
console.log('Browser started with extension enabled');
});
});
Testing Installation Flow
Simulating Fresh Install:
- Go to
chrome://extensions - Remove extension
- Reload unpacked extension
onInstalledfires withreason: 'install'
Simulating Update:
- Change version in
wxt.config.ts - Restart dev server
onInstalledfires withreason: 'update'
Common Lifecycle Patterns
Setting Defaults
browser.runtime.onInstalled.addListener(async (details) => {
if (details.reason === 'install') {
// Initialize with defaults
await storage.setItem('sync:settings', {
theme: 'light',
notifications: true
});
}
});
Analytics Initialization
browser.runtime.onInstalled.addListener(async (details) => {
await storage.setItem('local:installDate', new Date().toISOString());
await storage.setItem('local:installReason', details.reason);
});
What’s Next
You now understand extension lifecycle management, ensuring TabNotes initializes properly and handles updates gracefully. The extension is functionally complete.
Lesson 13 adds polish - final UX improvements, bug fixes, and quality-of-life features before submitting to the Chrome Web Store.