Chrome Extension Multi Tab Coordination — Best Practices

6 min read

Multi-Tab Coordination Patterns

Overview

Coordinating behavior across multiple tabs is a common extension pattern. The background service worker acts as the central coordinator, broadcasting events, managing shared state, and ensuring consistency. This guide covers production patterns for tab coordination.

Permissions: Requires "tabs" permission. Storage sync uses "storage".


Pattern 1: Broadcast to All Tabs

The background script can send messages to all extension tabs:

// background.ts
async function broadcastToAllTabs(message: object): Promise<void> {
  const tabs = await chrome.tabs.query({});
  
  await Promise.all(
    tabs
      .filter(tab => tab.id && !tab.incognito)
      .map(tab => chrome.tabs.sendMessage(tab.id!, message))
  );
}

// Notify all tabs when extension state changes
export function broadcastStateUpdate(state: AppState): void {
  broadcastToAllTabs({ type: "STATE_UPDATE", payload: state });
}

Pattern 2: Selective Broadcast by URL Pattern

Filter tabs by URL before broadcasting:

async function broadcastByPattern(pattern: string, message: object): Promise<void> {
  const tabs = await chrome.tabs.query({ url: pattern });
  
  await Promise.all(
    tabs.map(tab => tab.id && chrome.tabs.sendMessage(tab.id, message))
  );
}

// Example: Notify only tabs on example.com
broadcastByPattern("*://*.example.com/*", { type: "REFRESH_DATA" });

Pattern 3: Tab-Specific State with tabId Keys

Store per-tab state using tabId as the key:

// background.ts
const tabState = new Map<number, TabContext>();

chrome.tabs.onCreated.addListener((tab) => {
  if (tab.id) {
    tabState.set(tab.id, { initialized: false, data: null });
  }
});

chrome.tabs.onRemoved.addListener((tabId) => {
  tabState.delete(tabId);
});

function getTabContext(tabId: number): TabContext | undefined {
  return tabState.get(tabId);
}

Pattern 4: Leader Election for Exclusive Operations

Ensure only one tab performs an exclusive operation:

// background.ts
let currentLeaderTabId: number | null = null;

async function electLeader(tabId: number): Promise<boolean> {
  if (currentLeaderTabId === null) {
    currentLeaderTabId = tabId;
    chrome.tabs.sendMessage(tabId, { type: "YOU_ARE_LEADER" });
    return true;
  }
  
  chrome.tabs.sendMessage(tabId, { type: "ELECTION_FAILED", leader: currentLeaderTabId });
  return false;
}

chrome.tabs.onRemoved.addListener((tabId) => {
  if (tabId === currentLeaderTabId) {
    currentLeaderTabId = null; // Re-election needed
  }
});

Pattern 5: Cross-Tab State Synchronization

Use storage.onChanged to sync state across all contexts:

// background.ts
chrome.storage.onChanged.addListener((changes, area) => {
  if (area !== "sync" && area !== "local") return;
  
  const stateChange = changes["appState"];
  if (!stateChange) return;
  
  broadcastToAllTabs({
    type: "STORAGE_SYNC",
    oldValue: stateChange.oldValue,
    newValue: stateChange.newValue
  });
});

Pattern 6: Tab Counting & Duplicate Detection

Track tabs per domain and detect duplicates:

async function getTabCountByDomain(): Promise<Map<string, number>> {
  const tabs = await chrome.tabs.query({});
  const counts = new Map<string, number>();
  
  for (const tab of tabs) {
    if (!tab.url) continue;
    try {
      const domain = new URL(tab.url).hostname;
      counts.set(domain, (counts.get(domain) || 0) + 1);
    } catch {}
  }
  
  return counts;
}

async function hasDuplicate(url: string): Promise<boolean> {
  const tabs = await chrome.tabs.query({ url: `${new URL(url).origin}*` });
  return tabs.length > 1;
}

Pattern 7: Focus Management

Coordinate tab focus across the extension:

async function focusTabByUrl(pattern: string): Promise<void> {
  const [tab] = await chrome.tabs.query({ url: pattern, active: true });
  
  if (tab?.id) {
    await chrome.tabs.update(tab.id, { active: true });
    if (tab.windowId) {
      await chrome.windows.update(tab.windowId, { focused: true });
    }
  }
}

Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.