Content Scripts vs Background Workers — When to Use Each
10 min readContent Scripts vs Background Workers — When to Use Each
Introduction
Understanding the distinction between content scripts and background workers is fundamental to Chrome extension architecture. These two components serve different purposes and communicate through message passing. Choosing the right component for each task directly impacts your extension’s performance, security, and maintainability.
What Are Content Scripts?
Content scripts are JavaScript files that run in the context of web pages. They can read and modify the DOM, access some page variables, and communicate with the extension’s background service worker.
{
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"css": ["styles.css"],
"run_at": "document_idle"
}
]
}
Content Script Characteristics
- Page Context: Runs within the web page’s DOM
- DOM Access: Full read/write access to page elements
- Limited APIs: Can use only a subset of Chrome APIs
- Per-Tab Execution: Runs in every matching tab
- Isolated Worlds: Executes in an isolated world (MV2) or main world (MV3)
// Content script example
const header = document.querySelector('h1');
header.style.backgroundColor = '#ff0000';
document.addEventListener('click', (e) => {
chrome.runtime.sendMessage({ action: 'trackClick', target: e.target.tagName });
});
What Are Background Workers?
Background workers (service workers in Manifest V3) run in the background and manage extension state, handle events, and coordinate between different parts of the extension.
{
"background": {
"service_worker": "background.js"
}
}
Background Worker Characteristics
- No DOM Access: Cannot interact with web page content
- Full Extension APIs: Access to all Chrome extension APIs
- Event-Driven: Responds to browser and extension events
- Central Coordinator: Manages communication between components
- Ephemeral Lifecycle: Terminates when idle (MV3)
// Background service worker example
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'trackClick') {
console.log('Click tracked:', message.target);
// Process and store data
}
});
chrome.alarms.create('periodicCleanup', { periodInMinutes: 15 });
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'periodicCleanup') {
// Perform cleanup tasks
}
});
Comparison Table
| Feature | Content Scripts | Background Workers |
|---|---|---|
| DOM Access | Full access | No access |
| Chrome APIs | Limited subset | Full access |
| Page Context | Injected into pages | Separate context |
| Lifetime | Per-tab, page lifetime | Extension lifetime |
| Persistence (MV3) | Terminates with page | Terminates when idle |
| Storage Access | chrome.storage | chrome.storage |
| Network Requests | Page’s CORS rules | Extension’s permissions |
When to Use Content Scripts
Direct Page Interaction
Content scripts are essential when you need to:
- Manipulate page DOM elements
- Read or modify page CSS
- Inject custom styles
- Extract data from pages
// Extracting page data
const prices = Array.from(document.querySelectorAll('.price'))
.map(el => parseFloat(el.textContent));
chrome.runtime.sendMessage({ action: 'pricesFound', prices });
Page-Specific UI
Add custom UI elements directly to web pages:
// Inject a floating button
const button = document.createElement('button');
button.className = 'my-extension-btn';
button.textContent = 'Save to Collection';
document.body.appendChild(button);
button.addEventListener('click', () => {
// Save action
});
Real-Time Page Monitoring
Monitor page changes and user interactions:
// Monitor DOM mutations
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes.length > 0) {
chrome.runtime.sendMessage({ action: 'domChanged' });
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
When to Use Background Workers
Extension-Wide State Management
Background workers maintain state across all tabs and windows:
// Global state in background
let extensionState = {
user: null,
settings: {},
activeTabs: new Set()
};
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
switch (message.type) {
case 'UPDATE_STATE':
extensionState = { ...extensionState, ...message.data };
break;
case 'GET_STATE':
sendResponse(extensionState);
break;
}
});
Long-Running Tasks
Handle operations that persist beyond individual page sessions:
// Periodic data sync
chrome.alarms.create('syncData', { periodInMinutes: 5 });
chrome.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name === 'syncData') {
const data = await fetchLatestData();
await chrome.storage.local.set({ cachedData: data });
}
});
API Requests with Extended Permissions
Make requests that require extension permissions:
// Background can make cross-origin requests
async function fetchWithExtension(url) {
const response = await fetch(url);
const data = await response.json();
return data;
}
// Handle messages from content scripts
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'fetchData') {
fetchWithExtension(message.url).then(sendResponse);
return true; // Keep channel open for async response
}
});
Communication Patterns
Content Script to Background
// From content script
chrome.runtime.sendMessage({ action: 'analyzePage', url: window.location.href });
// In background
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'analyzePage') {
// Process page analysis
}
});
Background to Content Script
// From background - send to specific tab
chrome.tabs.sendMessage(tabId, { action: 'highlightElements', selector: '.product' });
// In content script
chrome.runtime.onMessage.addListener((message, sender) => {
if (message.action === 'highlightElements') {
document.querySelectorAll(message.selector).forEach(el => {
el.style.border = '2px solid red';
});
}
});
Long-Lived Connections
// Create port for ongoing communication
// In content script
const port = chrome.runtime.connect({ name: 'popup' });
port.postMessage({ action: 'getData' });
port.onMessage.addListener((message) => {
console.log('Received:', message);
});
// In background
chrome.runtime.onConnect.addListener((port) => {
port.onMessage.addListener((message) => {
if (message.action === 'getData') {
port.postMessage({ data: 'response' });
}
});
});
Performance Considerations
Content Script Efficiency
// Bad: Heavy processing on every mutation
const observer = new MutationObserver(() => {
heavyComputation(); // Runs on every DOM change
});
// Good: Debounced processing
let timeout;
const observer = new MutationObserver(() => {
clearTimeout(timeout);
timeout = setTimeout(heavyComputation, 300);
});
Background Worker Efficiency
// Bad: Persistent connections
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
const ws = new WebSocket('wss://example.com'); // Created on each message
});
// Good: Single connection, reused
let ws;
function getWebSocket() {
if (!ws || ws.readyState === WebSocket.CLOSED) {
ws = new WebSocket('wss://example.com');
}
return ws;
}
Architecture Best Practices
Separation of Concerns
| Component | Responsibility |
|---|---|
| Content Scripts | Page interaction, DOM manipulation |
| Background | State, coordination, API calls |
| Popup | Quick actions, current state display |
| Options Page | Configuration management |
Security Considerations
- Validate all messages: Never trust data from content scripts
- Minimize privileges: Use minimum necessary permissions
- Content Security Policy: Adhere to MV3 CSP requirements
// Always validate in background
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
// Validate sender tab
if (!sender.tab) return;
// Validate message structure
if (!message.action || typeof message.action !== 'string') return;
// Process validated message
});
Conclusion
Content scripts and background workers serve complementary roles in Chrome extensions. Use content scripts for direct page interaction and DOM manipulation. Use background workers for state management, cross-tab coordination, and extended API access. Effective extensions leverage both components through clean message passing architecture.
For more on extension architecture, see our Background Patterns and Content Scripts Deep Dive guides.