Back
Lesson 12

Web Extension Lifecycle

View Source Code

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:

  1. Event Triggers → Worker starts
  2. Handler Executes → Code runs
  3. 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:

  1. Go to chrome://extensions
  2. Remove extension
  3. Reload unpacked extension
  4. onInstalled fires with reason: 'install'

Simulating Update:

  1. Change version in wxt.config.ts
  2. Restart dev server
  3. onInstalled fires with reason: '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.