Chrome Extension Extension Lifecycle Events — Best Practices
5 min readExtension Lifecycle Events
This guide covers handling Chrome extension lifecycle events comprehensively: installation, updates, startup, and shutdown.
Prerequisites
{
"manifest_version": 3,
"permissions": ["storage", "contextMenus", "declarativeContent"]
}
chrome.runtime.onInstalled
Fires once when the extension is installed or updated. Use for one-time initialization.
// background/service-worker.ts
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === 'install') {
// First-time setup
handleFirstInstall();
} else if (details.reason === 'update') {
// Handle extension update
handleUpdate(details.previousVersion);
}
});
async function handleFirstInstall(): Promise<void> {
// Set default settings (idempotent)
await initializeDefaultSettings();
// Create context menus
await setupContextMenus();
// Set up declarativeContent rules
await setupDeclarativeRules();
// Open onboarding tab
chrome.tabs.create({ url: 'onboarding.html' });
}
async function handleUpdate(previousVersion: string): Promise<void> {
// Run migrations
await runMigrations(previousVersion);
// Show "what's new" notification
await showWhatsNew(previousVersion);
}
Initialization Best Practices
Idempotent Storage Initialization
Always check before setting defaults to support users who have customized settings:
async function initializeDefaultSettings(): Promise<void> {
const defaults = {
theme: 'system',
notifications: true,
syncEnabled: false
};
const stored = await chrome.storage.local.get(Object.keys(defaults));
// Only set defaults for keys that don't exist
const toSet: Record<string, unknown> = {};
for (const [key, value] of Object.entries(defaults)) {
if (stored[key] === undefined) {
toSet[key] = value;
}
}
if (Object.keys(toSet).length > 0) {
await chrome.storage.local.set(toSet);
}
}
Context Menu Recreation
Always recreate context menus in onInstalled to handle extension updates:
async function setupContextMenus(): Promise<void> {
// Clear existing to avoid duplicates
await chrome.contextMenus.removeAll();
chrome.contextMenus.create({
id: 'analyze-page',
title: 'Analyze This Page',
contexts: ['page']
});
chrome.contextMenus.create({
id: 'analyze-selection',
title: 'Analyze Selection',
contexts: ['selection']
});
}
chrome.runtime.onStartup
Runs when Chrome starts (profile starts). Use for restoring state:
chrome.runtime.onStartup.addListener(() => {
console.log('Chrome started, initializing extension...');
// Restore any cached state
initializeFromStorage();
// Re-register listeners that may have been cleared
setupEventListeners();
});
chrome.runtime.onSuspend
Fires just before the service worker is terminated. Save critical state:
chrome.runtime.onSuspend.addListener(() => {
// Save current state before SW stops
saveCurrentState();
// Persist any pending operations
flushPendingChanges();
});
Listener Registration: Common Mistake
Always register listeners at top level, not inside callbacks:
// ❌ WRONG - listeners won't receive events
chrome.runtime.onInstalled.addListener(() => {
chrome.runtime.onMessage.addListener(handleMessage);
});
// ✅ CORRECT - listeners registered at top level
chrome.runtime.onInstalled.addListener(handleInstall);
chrome.runtime.onMessage.addListener(handleMessage);
Complete Lifecycle Handler
// background/service-worker.ts
// Top-level listener registration
chrome.runtime.onInstalled.addListener(handleInstall);
chrome.runtime.onStartup.addListener(handleStartup);
chrome.runtime.onSuspend.addListener(handleSuspend);
async function handleInstall(details: chrome.runtime.InstalledDetails): Promise<void> {
if (details.reason === 'install') {
await initializeDefaults();
await setupContextMenus();
await setupDeclarativeRules();
} else if (details.reason === 'update') {
await runMigrations(details.previousVersion);
await showUpdateNotification(details.previousVersion);
}
}
async function handleStartup(): Promise<void> {
// Always re-register listeners on startup
await restoreState();
registerContentScripts();
}
function handleSuspend(): void {
// Critical: save state synchronously if needed
persistState();
}
Event Ordering
On first install, only chrome.runtime.onInstalled fires – chrome.runtime.onStartup does not fire during the initial installation. onStartup fires on subsequent browser/profile starts after the extension is already installed. Both events are independent and serve different purposes.
Cross-References
Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.