Browser Extension Messaging Basics
How content scripts, background/service workers, and pages talk: runtime messaging, ports, and security boundaries.
- browser
- extensions
- messaging
- chrome-api
Browser extensions are multi-process apps. A content script shares the page’s DOM world (with isolation rules) but not the page’s JS globals. A background service worker (Manifest V3) owns long-lived logic. A popup is a short-lived document. Messaging is how these pieces cooperate without sharing memory.
Docs: Chrome extension messaging, MDN webextension messaging.
The isolation map
| Context | Sees page DOM? | Sees page JS? | Extension APIs |
|---|---|---|---|
| Page script | Yes | Yes | No |
| Content script | Yes (usually) | No (isolated world) | Limited |
| Service worker | No | No | Full |
| Popup / options | Own document | Own JS | Full |
“I set window.foo in the page and the content script can’t read it” is expected. Use DOM custom events, window.postMessage, or the official messaging APIs deliberately.
One-shot messages
// content-script.js
chrome.runtime.sendMessage({ type: 'GET_STATUS' }, (response) => {
console.log(response);
});
// service-worker.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type !== 'GET_STATUS') return;
sendResponse({ ok: true, tabId: sender.tab?.id });
// return true if you will sendResponse asynchronously
});
Always validate message.type and shape. Treat messages as untrusted input even inside your extension — a compromised page or buggy script can still send noise if you exposed a bridge.
Long-lived ports
const port = chrome.runtime.connect({ name: 'watch' });
port.postMessage({ type: 'subscribe', topic: 'cart' });
port.onMessage.addListener((msg) => {
/* stream updates */
});
Ports suit streams and multi-step flows. Handle port.onDisconnect — service workers stop, popups close, tabs navigate.
Talking to the page
Content scripts can inject a script tag (carefully) or use postMessage:
// content script
window.addEventListener('message', (event) => {
if (event.source !== window) return;
if (event.data?.source !== 'my-extension') return;
// handle
});
window.postMessage({ source: 'my-extension', type: 'READY' }, '*');
Prefer a strict origin and a shared secret/token pattern when the page is first-party and controlled. Never accept eval of message payloads.
External web pages → extension
externally_connectable (Chrome) lets listed origins call chrome.runtime.sendMessage(extensionId, …). Whitelist origins tightly. This is a common phishing/social-engineering surface if misconfigured.
Manifest V3 footguns
- Service workers are ephemeral — don’t keep critical state only in SW memory; use
chrome.storage. - Alarms and events rehydrate the worker; design handlers to be restart-safe.
- Host permissions are required for cross-origin fetches from the SW.
chrome.scripting.executeScriptneeds permission and a clear target.
Interview out-loud
“Content scripts are isolated from page JS; they message the service worker with runtime.sendMessage or ports. Validate message shapes, handle disconnect, and never trust page postMessage without origin and schema checks.”
Minimal architecture that scales
- Content script: DOM read/write only; no secrets.
- Service worker: orchestrates network, storage, alarms.
- Popup: ephemeral UI; hydrate from
chrome.storageevery open. - Messages: versioned (
v: 1) with Zod-like runtime checks.
function assertMsg(msg) {
if (!msg || msg.v !== 1 || typeof msg.type !== 'string') {
throw new Error('bad message');
}
}
Never inject world-main scripts that read extension IDs into untrusted pages without a solid threat model — page JS can listen and spoof UI.
Related
Further reading
Related guides
- BFCache Back Forward CacheHow the back/forward cache freezes pages for instant history nav, what blocks it, and how to restore state safely.
- Browser DevTools Network PanelRead waterfalls, timing phases, headers, throttling, and initiator chains in the Network panel like a production debugger.
- Browser DevTools Performance PanelRecord main-thread timelines: long tasks, style/layout/paint, frames, and how to turn flame charts into INP fixes.
- Browser Networking 101DNS, TCP/TLS, HTTP/1.1 vs H2/H3, connection reuse, and what frontend code can actually influence.
- Browser Storage ComparisonCookies, localStorage, sessionStorage, IndexedDB, Cache Storage — capacity, persistence, and when each is the wrong tool.