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:
- Isolated World (Default):
// manifest.json
{
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["content.js"]
}]
}
- Main World (Page Context):
// manifest.json
{
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["content.js"],
"world": "MAIN"
}]
}
chrome.scripting
Chrome’s scripting API provides dynamic injection:
await chrome.scripting.executeScript({
target: { tabId },
func: () => {
// Your code here
}
});
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"
}
]
}
Dynamic injection
chrome.tabs.executeScript({
file: 'dynamic.js',
runAt: 'document_start'
});
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
}
});
}
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();
}
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
}
});
}
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
}
});
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
};
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
};
}
});
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
});
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;
}
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`);
});
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`);
});
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:
- You need persistent monitoring
- You want automatic injection on page load
- You’re maintaining state across operations
- You need early page load intervention
- You want declarative injection via manifest
Use chrome.scripting When:
- You need one-time operations
- You want to pass data directly to injected code
- You’re doing heavy DOM manipulation
- You need cleaner error handling
- You want better performance for occasional operations
Use Both When:
- Building complex features
- Handling both persistent and one-off tasks
- Managing state while doing heavy lifting
- 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
Top comments (0)