Content Scripts vs Background Workers — When to Use Each

10 min read

Content 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

// 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

// 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:

// 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

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

No previous article
No next article