The background service worker is the extension's event hub. Chrome 1 starts it for events and stops it after 30 seconds without events or API calls, so register listeners at the top level and keep state in chrome.storage, not in globals. Content scripts run inside matching pages in an isolated world: they share the page's DOM but not its JavaScript variables, and they can call only a few extension APIs such as runtime.sendMessage(). Everything else travels by message.

const links = [...document.querySelectorAll('a[href]')];
const external = links.filter((a) => a.hostname && a.hostname !== location.hostname).length;
const msg = { type: 'links', title: document.title, total: links.length, external };
chrome.runtime.sendMessage(msg).then((reply) => console.log('[content] badge =', reply.badge));chrome.runtime.onInstalled.addListener(({ reason }) => console.log('[sw] onInstalled:', reason));
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type !== 'links') return;
const tabId = sender.tab.id;
const text = String(msg.total);
chrome.action.setBadgeText({ tabId, text });
chrome.storage.session.set({ [`tab-${tabId}`]: msg }); // survives service worker restarts
console.log(`[sw] tab ${tabId}: ${msg.total} links, ${msg.external} external`);
sendResponse({ badge: text }); // or return true to reply later
});[sw] onInstalled: install [sw] tab 55599464: 7 links, 3 external [content] badge = 7
This output is real: Playwright 26,280 loaded the folder into Chromium 151 4,389 with --load-extension, a flag branded Chrome ignores since version 137. The [sw] lines appear in the service worker's console (linked from chrome://extensions). From Chrome 148 a listener may return a Promise, and every API is also exposed as browser.*.