DEV Community

Kristian Ivanov
Kristian Ivanov

Posted on • Originally published at levelup.gitconnected.com on

Content Scripts vs chrome.scripting: Understanding Modern Extension Development


Photo by Swello on Unsplash

While building extensions, I keep seeing a common question pop up: “Should I use content scripts or chrome.scripting?” The answer isn’t as straightforward as you might think, especially since content scripts are more powerful than many developers realize. Let’s dive into the real differences and when to use each approach.

The background/info

Content Scripts

 — Released: December 2009 (Chrome Extensions Launch)

 — Part of the original extension system

 — Updated in Manifest V2 (2012) with more options

 — Updated in Manifest V3 (2021) with additional features like world: "MAIN"

chrome.scripting

 — 
Released: January 2021 (with Manifest V3)

 — Designed to replace chrome.tabs.executeScript

 — Part of the move away from background pages to service workers

 — Considered the modern approach to script injection

The Basics

Both content scripts and chrome.scripting let you inject code into web pages, but they serve different purposes and have different strengths.

Content Scripts

Content scripts can work in two ways:

  1. Isolated World (Default):
// manifest.json
{
 "content_scripts": [{
 "matches": ["<all_urls>"],
 "js": ["content.js"]
 }]
}
Enter fullscreen mode Exit fullscreen mode
  1. Main World (Page Context):
// manifest.json
{
 "content_scripts": [{
 "matches": ["<all_urls>"],
 "js": ["content.js"],
 "world": "MAIN"
 }]
}
Enter fullscreen mode Exit fullscreen mode

chrome.scripting

Chrome’s scripting API provides dynamic injection:

await chrome.scripting.executeScript({
 target: { tabId },
 func: () => {
 // Your code here
 }
});
Enter fullscreen mode Exit fullscreen mode

The Real Differences

1. Execution Timing

Content Scripts:

 — Load automatically when pages match

 — Can run at document_start, document_end, or document_idle

 — Stay active throughout page lifetime

 — Can be dynamically injected too

Manifest-based injection

{
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["early.js"],
      "run_at": "document_start"
    },
    {
      "matches": ["<all_urls>"],
      "js": ["late.js"],
      "run_at": "document_idle"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Dynamic injection

chrome.tabs.executeScript({
  file: 'dynamic.js',
  runAt: 'document_start'
});
Enter fullscreen mode Exit fullscreen mode

chrome.scripting:

 — Runs on demand

 — Single execution context

 — Cleanup after execution

 — Better for one-off tasks

async function modifyPage(tabId) {
  await chrome.scripting.executeScript({
    target: { tabId },
    func: () => {
      // Do something once and finish
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

2. State Management

Content Scripts:

let state = {
  observers: new Set(),
  modifications: new Map()
};

// State persists across functions
function addObserver(target) {
  const observer = new MutationObserver(() => {
    // Handle changes
  });
  observer.observe(target);
  state.observers.add(observer);
}

function cleanup() {
  state.observers.forEach(o => o.disconnect());
  state.observers.clear();
}
Enter fullscreen mode Exit fullscreen mode

chrome.scripting:

// Each execution gets fresh state
async function observePage(tabId) {
  await chrome.scripting.executeScript({
    target: { tabId },
    func: () => {
      // State only lives for this execution
      const observer = new MutationObserver(() => {
        // Handle changes
      });
      observer.observe(document.body);

      // State is lost after execution
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

3. Context Access

Content Scripts (Isolated):

// content.js (default world)
// No access to page's variables
window.pageVariable; // undefined

// Need message passing for page communication
window.postMessage({ type: 'getData' }, '*');
window.addEventListener('message', (e) => {
  if (e.data.type === 'response') {
    // Handle data
  }
});
Enter fullscreen mode Exit fullscreen mode

Content Scripts (Main World):

// content.js with world: "MAIN"
// Direct access to page context
window.pageVariable; // Works!
document.querySelector('#app').shadowRoot; // Works!

// Can modify page functions
window.originalFunc = window.someFunc;
window.someFunc = function() {
  // Modified behavior
};
Enter fullscreen mode Exit fullscreen mode

chrome.scripting:

chrome.scripting.executeScript({
  target: { tabId },
  func: () => {
    // Always runs in page context
    window.pageVariable; // Works!

    // Can modify prototype methods
    Element.prototype.originalRemove = Element.prototype.remove;
    Element.prototype.remove = function() {
      // Custom remove logic
    };
  }
});
Enter fullscreen mode Exit fullscreen mode

4. Performance Considerations

Content Scripts:

Loaded with the page, always running

const observer = new MutationObserver(() => {
  // Constant monitoring
});

observer.observe(document.body, {
  childList: true,
  subtree: true
});
Enter fullscreen mode Exit fullscreen mode

chrome.scripting:

Only runs when needed

async function checkPage(tabId) {
  const results = await chrome.scripting.executeScript({
    target: { tabId },
    func: () => {
      // Quick operation and done
      return document.querySelectorAll('.target').length;
    }
  });
  return results[0].result;
}
Enter fullscreen mode Exit fullscreen mode

Real World Example

Element Removal

Content Script Approach:

// manifest.json
{
 "content_scripts": [{
 "matches": ["<all_urls>"],
 "js": ["darkmode.js"],
 "world": "MAIN" // We want page context
 }]
}

// content.js - Always running, waiting for commands
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === 'removeElement') {
    const { selector } = message;
    const elements = document.querySelectorAll(selector);
    const count = elements.length;

    elements.forEach(el => el.remove());
    sendResponse({ count });
  }
});

// background.js - Sends command to content script
async function removeElement(tabId, selector) {
  return new Promise((resolve) => {
    chrome.tabs.sendMessage(tabId, {
      type: 'removeElement',
      selector
    }, (response) => {
      resolve(response?.count || 0);
    });
  });
}


// Usage:
chrome.action.onClicked.addListener(async (tab) => {
  const count = await removeElement(tab.id, '.ad-container');
  console.log(`Removed ${count} elements`);
});
Enter fullscreen mode Exit fullscreen mode

chrome.scripting Approach:

// background.js - Direct injection and execution
async function removeElement(tabId, selector) {
  const results = await chrome.scripting.executeScript({
    target: { tabId },
    args: [selector],
    func: (selector) => {
      const elements = document.querySelectorAll(selector);
      const count = elements.length;
      elements.forEach(el => el.remove());
      return count;
    }
  });

  return results[0].result;
}

// Usage - exactly the same:
chrome.action.onClicked.addListener(async (tab) => {
  const count = await removeElement(tab.id, '.ad-container');
  console.log(`Removed ${count} elements`);
});
Enter fullscreen mode Exit fullscreen mode

Final Thoughts

The choice between content scripts and chrome.scripting isn’t mutually exclusive. Modern extensions often use both:

— Content scripts for persistent features and state management

 — Chrome.scripting for heavy lifting and one-off operations

 — Communication between the two for complex features

The key is understanding their strengths:

Use Content Scripts When:

  1. You need persistent monitoring
  2. You want automatic injection on page load
  3. You’re maintaining state across operations
  4. You need early page load intervention
  5. You want declarative injection via manifest

Use chrome.scripting When:

  1. You need one-time operations
  2. You want to pass data directly to injected code
  3. You’re doing heavy DOM manipulation
  4. You need cleaner error handling
  5. You want better performance for occasional operations

Use Both When:

  1. Building complex features
  2. Handling both persistent and one-off tasks
  3. Managing state while doing heavy lifting
  4. Optimizing performance for different operations

If you found this article helpful, feel free to clap and follow for more JavaScript and Chrome.API tips and tricks.

You can also give a recent chrome extension I released that uses a lot of the functionalities from the articles — Web à la Carte

If you have gotten this far, I thank you and I hope it was useful to you! Here is a cool image of a cat (no scripting pun though) as a thank you!


Photo by Chris Barbalis on Unsplash


Sentry image

Hands-on debugging session: instrument, monitor, and fix

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

RSVP here →

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay