Chrome Extension Tab Lifecycle Management — Best Practices

7 min read

Tab 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.