Implementation of Content Script (page modification) in Browser Extension
Content Script is a JavaScript file that the browser injects into the target page context. It works in an isolated environment ("isolated world"): has access to page DOM, but not to JS variables of the page itself. This is both protection and limitation.
How browser loads content script
In manifest.json (MV3) declare list of scripts and conditions for their execution:
{
"manifest_version": 3,
"content_scripts": [
{
"matches": ["https://*.example.com/*", "https://other-site.com/app/*"],
"js": ["content/injected.js"],
"css": ["content/injected.css"],
"run_at": "document_idle",
"all_frames": false,
"world": "ISOLATED"
}
]
}
run_at accepts three values: document_start (before DOM construction), document_end (DOM ready, resources loading), document_idle (after DOMContentLoaded — safe default).
For dynamic injection from service worker:
// background/service-worker.js
chrome.action.onClicked.addListener(async (tab) => {
await chrome.scripting.executeScript({
target: { tabId: tab.id, allFrames: false },
files: ['content/injected.js'],
world: 'ISOLATED' // or 'MAIN' for page JS context access
});
});
world: 'MAIN' gives access to page variables, but loses isolation — use only when necessary (API interception, monkey-patching).
Working with DOM
Content script sees full DOM. Simple task — highlight all prices on page:
// content/price-highlighter.js
function highlightPrices() {
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
{
acceptNode(node) {
return /\$[\d,]+\.?\d{0,2}/.test(node.textContent)
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_SKIP;
}
}
);
const nodes = [];
while (walker.nextNode()) nodes.push(walker.currentNode);
nodes.forEach(node => {
const span = document.createElement('span');
span.innerHTML = node.textContent.replace(
/(\$[\d,]+\.?\d{0,2})/g,
'<mark class="ext-price-highlight">$1</mark>'
);
node.parentNode.replaceChild(span, node);
});
}
// Page may load content via XHR/fetch after DOMContentLoaded
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.addedNodes.length > 0) {
highlightPrices();
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
highlightPrices();
Communication with background
// content/index.js
chrome.runtime.sendMessage({ type: 'PAGE_DATA', url: window.location.href }, (response) => {
console.log('Background replied:', response);
});
// Listen for messages from background
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'PERFORM_ACTION') {
performAction(message.payload);
sendResponse({ success: true });
}
});
Timeline
Simple DOM highlighter or injector: 1–2 days. Complex parser with background sync: 3–5 days.







