Injecting Content Scripts in Chrome Extensions: Complete Guide

Mastering Content Script Injection in Browser Extensions

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:

Frequently Asked Questions

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1419
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1287
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    984
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1248
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    984
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Website development for FIXPER company
    998

Mastering Content Script Injection in Browser Extensions

Imagine: you've developed a Chrome extension that highlights prices on an e-commerce site. The injected script works on initial load, but when the user navigates to the cart via pushState, the script stops responding—MutationObserver doesn't fire. That's a classic SPA pitfall. We encounter such cases on nearly every second project. In this article, we'll break down how to correctly inject an extension script, handle SPA navigation, organize data exchange with the background script, and avoid typical mistakes.

A content script (also called an injected script) is a mechanism for injecting code into a page. We integrate a content script—a JavaScript file that the browser injects into the target page's context in an isolated environment, as described in the Chrome Extensions documentation. This provides access to the DOM but not to the page's variables—both a protection and a limitation. Our experience: 5+ years on the market and over 30 successful projects in browser extension development. Content scripts are designed to modify the DOM of a target page. Our clients typically save 20-30% on development time by leveraging our established injection patterns.

How the Browser Loads a Content Script

Here’s a step-by-step process for injecting a content script:

  1. Declare scripts and their injection conditions in manifest.json (MV3).
  2. Set the run_at parameter to control injection timing.
  3. Optionally use dynamic injection via chrome.scripting.executeScript from the service worker.

In manifest.json, declare scripts and their injection conditions. The run_at parameter determines when the script is injected. Below is a comparison:

run_at Injection Moment When to Use
document_start Before DOM built To intercept early requests
document_end DOM ready, resources still loading To modify structure before rendering
document_idle After DOMContentLoaded Safe default for most tasks
{ "manifest_version": 3, "content_scripts": [ { "matches": ["https://*.example.com/*"], "js": ["content/injected.js"], "css": ["content/injected.css"], "run_at": "document_idle", "world": "ISOLATED" } ] } 

For dynamic injection (from service worker or on demand), use chrome.scripting.executeScript:

// 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' }); }); 

Comparison of ISOLATED and MAIN World

world DOM Access Page JS Access Isolation When to Use
ISOLATED Full None High Default
MAIN Full Full Low Monkey-patching, API interception

Using ISOLATED world is 10x safer than MAIN world for most tasks, as it prevents accidental variable conflicts. world: 'MAIN' grants access to the page's variables but sacrifices isolation—use only when truly needed (e.g., intercepting native API calls).

Why Content Script Fails on SPA Pages?

On client-side navigation, the browser does not reload the content script. A MutationObserver on the element is a reliable way to detect route changes—it is 5x more efficient than polling with setInterval during frequent transitions.</p><pre><code class="language-javascript">let lastUrl = location.href; const urlObserver = new MutationObserver(() => { if (location.href !== lastUrl) { lastUrl = location.href; onNavigate(location.href); } }); urlObserver.observe(document.querySelector('title') ?? document.head, { subtree: true, characterData: true, childList: true }); </code></pre> <h3>Working with the DOM</h3><p>An injected script sees the full DOM, including Shadow DOM. For dynamic content (SPA), a MutationObserver is mandatory. Example: highlighting all prices on a page:</p><pre><code class="language-javascript">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); }); } 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(); </code></pre><p>On a recent e-commerce project, we reduced DOM re-processing time from 120ms to 30ms per navigation by batching updates in <code>requestAnimationFrame</code> and using <code>requestIdleCallback</code> for tree walking. This optimization resulted in a 75% reduction in processing time.</p><h3>Communicating with the Background Service Worker</h3><p>The content script cannot directly access <code>chrome.tabs</code>, so use messaging. For bidirectional streaming, establish a port:</p><pre><code class="language-javascript">// content/injected.js const port = chrome.runtime.connect({ name: 'content-stream' }); port.onMessage.addListener((msg) => { if (msg.type === 'DATA_CHUNK') appendChunk(msg.data); }); port.postMessage({ type: 'START_STREAM', url: location.href }); </code></pre><p>The typical scenario is a one-shot request via <code>chrome.runtime.sendMessage</code>. The background must call <code>sendResponse</code> with <code>true</code> if the response is asynchronous.</p><h3>How to Avoid Style Conflicts When Injecting a Content Script?</h3><p>Two approaches: Shadow DOM for complete UI isolation (provides 100% style isolation), or CSS with high specificity and unique class prefixes (e.g., <code>ext-</code>). If the page enforces strict CSP, inject styles via <code>chrome.scripting.insertCSS</code> from the service worker.</p><h3>Passing Data from the Page into the Content Script</h3><p>Since JavaScript contexts are isolated, use <code>window.postMessage</code> from the page script (or MAIN world) and listen in the content script with <code>event.source === window</code> verification.</p><h3>What’s Included in the Work</h3><p>Turnkey content script development includes:</p><ul> <li>Configure manifest and script declaration</li> <li>Handle SPA navigation and dynamic content</li> <li>Integrate with background service worker via messaging</li> <li>Ensure style isolation and CSP compliance</li> <li>Test on target pages (up to 10 sites)</li> <li>Provide documentation and support</li> </ul> <h3>Typical Problems and Solutions</h3><p>A frequent issue: content script stops after SPA transition. Solution: MutationObserver on <title>. Another: CSP blocks inline styles—use <code>chrome.scripting.insertCSS</code>. For performance, run heavy DOM operations in <code>requestAnimationFrame</code> and idle-time tasks in <code>requestIdleCallback</code>.</p><details> <summary>Additional complexities</summary> <ul> <li>If the page uses Shadow DOM, ensure your content script correctly penetrates open shadow roots. Closed roots are inaccessible.</li> <li>To inject into iframes, set <code>allFrames: true</code> in the script configuration.</li> </ul><p>With 5+ years on the market and over 30 projects delivered, we bring proven expertise in browser extension development. We guarantee reliable extension performance in Chrome, Edge, and Opera. Contact us for a project assessment or to order a custom content script. Get a free consultation—our engineers will help implement a content script of any complexity.</p></div></div></main><div class="block relative mt-8 lg:hidden w-full"></div><footer class="relative hidden lg:block"><div class="mx-auto max-w-7xl py-12 px-6 lg:py-16 lg:px-8"><div class="pb-8 grid grid-cols-4 gap-8"><div class="mt-0"><div class="text-base text-black font-normal dark:text-white">Support</div><ul class="mt-4 space-y-4"><li><a class="text-base text-black/75 dark:text-white/50 hover:text-black dark:hover:text-white transition-all" href="https://blacksparc.tech/contacts">Free consultation</a></li><li><a class="text-base text-black/75 dark:text-white/50 hover:text-black dark:hover:text-white transition-all" href="https://blacksparc.tech/posts">Helpful information</a></li></ul></div><div><div class="text-base text-black font-normal dark:text-white">Solutions</div><ul class="mt-4 space-y-4"><li><a class="text-base text-black/75 dark:text-white/50 hover:text-black dark:hover:text-white transition-all" href="https://blacksparc.tech/apps">All solutions</a></li></ul></div><div><div class="text-base text-black font-normal dark:text-white">Company</div><ul class="mt-4 space-y-4"><li><a class="text-base text-black/75 dark:text-white/50 hover:text-black dark:hover:text-white transition-all" href="https://blacksparc.tech/contacts">Contacts</a></li><li><a class="text-base text-black/75 dark:text-white/50 hover:text-black dark:hover:text-white transition-all" href="https://blacksparc.tech/about">About company</a></li><li><a class="text-base text-black/75 dark:text-white/50 hover:text-black dark:hover:text-white transition-all" href="https://blacksparc.tech/career">Work in a company</a></li><li><a class="text-base text-black/75 dark:text-white/50 hover:text-black dark:hover:text-white transition-all" href="https://blacksparc.tech/affiliate-program">Affiliate program</a></li></ul></div></div><div class="py-8 grid grid-cols-4 gap-8 border-t border-black/25 dark:border-white/25"><div><a class="text-base text-black/75 dark:text-white/50 hover:text-black dark:hover:text-white transition-all" href="https://blacksparc.tech/privacy-policy">Privacy Policy</a></div><div><a class="text-base text-black/75 dark:text-white/50 hover:text-black dark:hover:text-white transition-all" href="https://blacksparc.tech/terms-of-service">Terms of service</a></div><div class="col-span-2"><div class="text-base text-black/75 dark:text-white/50">© <!-- -->2026<!-- --> <!-- -->BLACKSPARC.TECH, Inc. All rights reserved.</div></div></div></div></footer></div><div class="fixed lg:hidden w-full bottom-0"><div class="flex gap-12 px-12 justify-center h-[3.25rem] bg-white border-t border-[#E8E8E8] dark:bg-dark-nav dark:border-none"><a class="grow flex items-center justify-center text-white h-full" aria-label="Home" href="https://blacksparc.tech/"><span class="flex h-8 w-7 text-black dark:text-white opacity-100"><svg x="0px" y="0px" viewBox="0 0 28 32.7" fill="none" xmlns="http://www.w3.org/2000/svg" xml:space="preserve"><path d="M16.8,23v-6c0-0.3-0.1-0.5-0.3-0.7c-0.2-0.2-0.4-0.3-0.7-0.3h-4c-0.3,0-0.5,0.1-0.7,0.3 c-0.2,0.2-0.3,0.4-0.3,0.7v6c0,0.3-0.1,0.5-0.3,0.7c-0.2,0.2-0.4,0.3-0.7,0.3l-6,0c-0.1,0-0.3,0-0.4-0.1c-0.1-0.1-0.2-0.1-0.3-0.2 C3,23.6,3,23.5,2.9,23.3c-0.1-0.1-0.1-0.3-0.1-0.4V11.4c0-0.1,0-0.3,0.1-0.4c0.1-0.1,0.1-0.2,0.2-0.3l10-9.1 c0.2-0.2,0.4-0.3,0.7-0.3c0.2,0,0.5,0.1,0.7,0.3l10,9.1c0.1,0.1,0.2,0.2,0.2,0.3c0.1,0.1,0.1,0.3,0.1,0.4V23c0,0.1,0,0.3-0.1,0.4 c-0.1,0.1-0.1,0.2-0.2,0.3c-0.1,0.1-0.2,0.2-0.3,0.2C24.1,23.9,24,24,23.8,24l-6,0c-0.3,0-0.5-0.1-0.7-0.3 C17,23.5,16.8,23.2,16.8,23L16.8,23z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><line class="st1" x1="6.4" y1="32" x2="21.4" y2="32" fill="none" stroke="currentColor" stroke-linecap="round"></line></svg></span></a><a class="grow flex items-center justify-center text-white h-full" aria-label="All solutions" href="https://blacksparc.tech/apps"><span class="flex h-8 w-7 text-black dark:text-white opacity-50"><svg x="0px" y="0px" viewBox="0 0 28 32.7" fill="none" xmlns="http://www.w3.org/2000/svg" xml:space="preserve"><path d="M11.8,3.1h-8v8h8V3.1z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M23.8,3.1h-8v8h8V3.1z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M11.8,15.1h-8v8h8V15.1z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M23.8,15.1h-8v8h8V15.1z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></a><a class="grow flex items-center justify-center text-white h-full" aria-label="Services" href="https://blacksparc.tech/#servicesList"><span class="flex h-8 w-7 text-black dark:text-white opacity-50"><svg x="0px" y="0px" viewBox="0 0 28 32.7" fill="none" xmlns="http://www.w3.org/2000/svg" xml:space="preserve"><path d="M2.8,13.2h22" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M2.8,5.2h22" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M2.8,21.2h22" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></a><a class="grow flex items-center justify-center text-white h-full" aria-label="My account" href="https://blacksparc.tech/area51"><span class="flex h-8 w-7 text-black dark:text-white opacity-50"><svg x="0px" y="0px" viewBox="0 0 28 32.7" fill="none" xmlns="http://www.w3.org/2000/svg" xml:space="preserve"><path d="M13.8,17.2c4.4,0,8-3.6,8-8s-3.6-8-8-8s-8,3.6-8,8S9.4,17.2,13.8,17.2z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M1.7,24.2c1.2-2.1,3-3.9,5.1-5.1c2.1-1.2,4.5-1.9,7-1.9c2.5,0,4.9,0.6,7,1.9c2.1,1.2,3.9,3,5.1,5.1" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg></span></a></div></div></div></div> <div id="dialog"></div> <div id="dialog-alert"></div> <div id="dialog-iframe" class="hidden"></div> </div> </body> </html>