Chrome Extension Tab Lifecycle Management — Best Practices
7 min readTab Lifecycle Management Patterns
Overview
Chrome extensions frequently need to track browser tabs throughout their lifecycle—from creation to removal, activation, and navigation. The chrome.tabs API provides comprehensive event listeners for monitoring these state changes. This guide covers practical patterns for implementing robust tab lifecycle management in your extension.
Core Tab Lifecycle Events
Event Overview
The Tabs API provides six primary lifecycle events:
| Event | Description |
|---|---|
| onCreated | Fired when a tab is created |
| onUpdated | Fired when a tab is updated (navigation, load complete) |
| onRemoved | Fired when a tab is closed |
| onActivated | Fired when the active tab changes in a window |
| onMoved | Fired when a tab is moved within a window |
| onReplaced | Fired when a tab is replaced by another (e.g.,Prerender) |
Basic Tab Tracker Implementation
Tracking All Tabs
Maintain a map of all open tabs:
// Background service worker
const tabTracker = new Map();
chrome.tabs.onCreated.addListener((tab) => {
tabTracker.set(tab.id, {
id: tab.id,
windowId: tab.windowId,
url: tab.url,
title: tab.title,
active: tab.active,
status: tab.status,
createdAt: Date.now()
});
console.log('Tab created:', tab.id);
});
chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
const removed = tabTracker.get(tabId);
if (removed) {
console.log('Tab removed:', tabId, 'byWindowClose:', removeInfo.isWindowClosing);
tabTracker.delete(tabId);
}
});
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (tabTracker.has(tabId)) {
const entry = tabTracker.get(tabId);
Object.assign(entry, {
url: changeInfo.url || entry.url,
title: tab.title,
status: changeInfo.status,
favIconUrl: tab.favIconUrl
});
}
});
Tab Activation Monitoring
Tracking Active Tab Changes
Detect when users switch tabs:
chrome.tabs.onActivated.addListener((activeInfo) => {
const { tabId, windowId } = activeInfo;
// Get previous active tab for this window
chrome.tabs.query({ windowId, active: false }, (inactiveTabs) => {
console.log(`Switched from tab to: ${tabId}`);
console.log('Other tabs in window:', inactiveTabs.length);
});
chrome.tabs.get(tabId, (tab) => {
console.log('New active tab URL:', tab.url);
});
});
Window Focus Tracking
Monitor when windows gain or lose focus:
chrome.windows.onFocusChanged.addListener((windowId) => {
if (windowId === chrome.windows.WINDOW_ID_NONE) {
console.log('No window focused');
return;
}
chrome.tabs.query({ windowId, active: true }, (tabs) => {
if (tabs[0]) {
console.log('Focused window active tab:', tabs[0].url);
}
});
});
URL Change Detection
Detecting Navigation Events
Track URL changes with the updated event:
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
// Wait for the tab to finish loading
if (changeInfo.status === 'complete' && changeInfo.url) {
console.log('Tab navigation complete:', changeInfo.url);
}
// Detect URL changes even without reload
if (changeInfo.url) {
console.log('URL changed:', changeInfo.url);
}
});
// For SPAs, detect fragment changes
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete') {
checkForHashChange(tabId, tab.url);
}
});
Filtered Listeners
Optimizing Event Handling
Use filters to reduce unnecessary callbacks:
// Only fires for tabs in specific windows
chrome.tabs.onActivated.addListener(
(activeInfo) => {
// Handle tab activation
},
{ windowId: chrome.windows.WINDOW_ID_CURRENT }
);
// Only fires when tabs complete loading
chrome.tabs.onUpdated.addListener(
(tabId, changeInfo, tab) => {
// Handle tab update
},
{ properties: ['status'] }
);
// Monitor specific URL patterns
chrome.tabs.onUpdated.addListener(
(tabId, changeInfo, tab) => {
if (tab.url?.startsWith('https://github.com')) {
console.log('GitHub tab updated:', tabId);
}
},
{ properties: ['url'] }
);
Tab State Management
Handling Tab States
Modern Chrome can discard tabs to save memory:
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
// Detect discarded tabs
if (changeInfo.discarded) {
console.log('Tab was discarded:', tabId);
// Re-activate to restore content
}
// Check if tab is currently discarded
if (tab.discarded) {
console.log('Tab is in discarded state');
}
});
// Detect when tabs become active after being discarded
chrome.tabs.onActivated.addListener((activeInfo) => {
chrome.tabs.get(activeInfo.tabId, (tab) => {
if (tab.discarded) {
console.log('Activating discarded tab - will reload');
}
});
});
Handling Tab Replacement
The onReplaced Event
Chrome sometimes replaces tabs (prerendering, instant pages):
chrome.tabs.onReplaced.addListener((addedTabId, removedTabId) => {
console.log(`Tab ${removedTabId} replaced by ${addedTabId}`);
// Update your tracking map
const removed = tabTracker.get(removedTabId);
if (removed) {
tabTracker.delete(removedTabId);
tabTracker.set(addedTabId, {
...removed,
id: addedTabId
});
}
});
Cross-References
Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.