Content Script and Service Worker: the Extension's Brain and Hands
In the previous lesson, you set up the extension's skeleton with manifest.json. In this lesson, meet the two most important roles in an extension: the service worker is the brain, staying in the background to respond to events; the content script is the hands and feet, getting into web pages to read and modify them. Only by distinguishing these two roles can you state your requirements clearly, and only then can the AI put the code in the right files.
What you'll be able to do after this lesson
- Explain clearly what content scripts and service workers can and cannot do
- Declare a background service worker and content_scripts in manifest.json and actually get them running
- Verify the "isolated world" yourself: the web page and your script cannot see each other's JS variables, yet they share the same DOM
- Know where to find each one's logs—this is the foundation for debugging in the next lesson
Your current predicament
- You asked AI to write an extension and it generated service-worker.js and content-script.js in one go, and you don't know why there are two or what each is responsible for
- Your script was clearly injected into the page but can't read the web page's own variables; you want to modify the page from the background, and AI says "that's not possible"
- You added console.log to your code but can't find the output: it's not on the extensions page, and it's not in the web page's console either
When to use this technique
This lesson is for you if you want to add the ability to "modify web pages" or "respond to browser events in the background" to your extension, or want to understand the file structure of AI-generated extensions.
And you don't want to: put code in the wrong files and redo it repeatedly, or have no idea where to look for logs when errors occur.
🎒 Preparation before starting
- Have finished the prerequisite course"manifest.json Explained: MV3 Field Quick Reference"
- Have an extension project on hand that can be loaded locally with Load unpacked (something like Hello Extensions works)
- Know how to open
chrome://extensionsand reload the extension (click the refresh icon on the extension's card) - Know how to open a web page's DevTools (right-click the page → "Inspect") and view the Console
Core idea
Think of the extension as a person:
- Service worker (the brain, i.e., the extension's background script): officially called the extension's "central event handler". It is not attached to any page and waits for events in the browser's background—closing tabs, clicking notifications, navigating to new pages are all its business; "loaded when needed, unloaded when idle". It cannot access the DOM; to change a page, it can only instruct the content script to do the work.
- Content script (the hands and feet): officially defined as "files that run in the web page's environment", using the standard DOM to read pages, modify pages, and pass information back to the extension itself.
- Isolated world: the content script lives in a private execution environment that the web page and other extensions cannot touch; it also cannot see the web page's JS variables. But the DOM is shared—both sides see the same page.
| Content script (hands and feet) | Service worker (brain) | |
|---|---|---|
| Where it runs | Inside a specific web page | In the browser's background, not attached to any page |
| Can it manipulate the DOM | ✅ Can read and modify | ❌ Cannot |
| Lifecycle | Follows the page | Starts on demand, stops when idle |
| Extension APIs directly available | A few (dom, i18n, storage, some runtime methods) | Acts as the event hub responding to all kinds of extension events |
| Typical tasks | Changing page styles, reading page content | Listening for tab/notification events, managing tabs |
| Where to view logs | The host web page's Console | Inspect views on the extensions page |
💡 Old tutorials don't match up?
The "background script" in old MV2 tutorials is today's service worker—the name changed, but the role didn't. Nowadays you only write MV3 extensions; don't copy old code verbatim.
Follow along
Step 1: Declare the service worker (the extension's background script)
Why: the brain must be registered first, so Chrome knows who to start in the background.
Add a background section in manifest.json:
"background": {
"service_worker": "service-worker.js"
}Create service-worker.js in the extension's root directory:
console.log("service worker 已启动");Go back to chrome://extensions and reload the extension.
You should see: an Inspect views area appears on the extension's card, with a blue service worker link next to it; click it and your log is sitting in the DevTools Console.
💡 Logs "disappear" after a while?
The service worker starts on demand and stops when idle. If it falls asleep after a while, that doesn't mean something is broken—it will wake up on its own the next time an event arrives.
- Quick check: you can see "service worker started" in the DevTools opened from Inspect views
Step 2: Write your first content script (static injection via content_scripts)
Why: Hands and feet don't grow on their own—you must statically declare them in the manifest with content_scripts for them to be automatically injected into the websites you specify.
Add this to manifest.json (matches is required and determines which pages get injected; the example below is from the official documentation—replace the domain with the site you actually want to target):
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"js": ["content-script.js"]
}
]The official example also includes a line "css": ["my-styles.css"]—a content script can inject only JS, only CSS, or both; pick what you need.
Create content-script.js in the extension's root directory:
console.log("[content script] 注入成功,页面标题:", document.title);
document.body.style.border = "5px solid red";Reload the extension, then refresh the target page—after changing a content script you must do both steps: "reload the extension + refresh the host page".
You should see: a red border appears around the target page; the page's DevTools Console shows the log you printed (note: it's the web page's Console, not the extension page's).
- Quick check: the red border appears, and the content script's log is visible in the page's Console
Step 3: Verify the isolated world with your own eyes
Why: This is where beginners get most confused—"the script clearly runs in the page, so why can't it read the page's variables?" The answer is the isolated world, and seeing it yourself is the most intuitive way to understand it.
Change content-script.js to:
var extVar = "我来自插件";
console.log("[content script] 我能读到页面标题:", document.title);
document.body.style.border = "5px solid red";
// 5 秒后再读网页变量,留时间给你在网页控制台定义它
setTimeout(function () {
console.log("[content script] 读网页变量 pageVar:", window.pageVar);
}, 5000);Reload the extension + refresh the page, then within 5 seconds switch to the page's Console and enter:
var pageVar = "我是网页自己的变量";After the 5-second log appears, enter extVar and document.body.style.border respectively in the page's Console.
You should see:
The content script prints
读网页变量 pageVar:undefined—it cannot see the page's variablesEntering
extVarin the page's Console throwsReferenceError—the page also cannot see your variablesdocument.body.style.borderreturns"5px solid red"—the DOM is shared by both sidesQuick check: you saw all three phenomena with your own eyes and can match them to the phrase "variables are isolated, DOM is shared"
Step 4: Assign tasks to the right role (when to use which)
Why: Only by distinguishing the brain from the hands and feet can you tell the AI your requirements correctly, so it won't put code in the wrong file.
| Your requirement | Who handles it |
|---|---|
| Inject code on specific pages, modify the page | content script |
| Page-independent: wait for events in the background, listen to browser behavior, pop notifications / manage tabs | service worker |
| Pop up a small window (popup) when the toolbar icon is clicked | action + popup |
| Intercept, block, or modify network requests | declarativeNetRequest |
Very few extension APIs are directly available to content scripts—the official list includes dom, i18n, storage, and some methods of runtime (such as runtime.sendMessage() and runtime.onMessage). To use other APIs, you must forward a message to the service worker to handle it on your behalf—which is why these two files often appear in pairs, not because the AI wrote extra code.
You should see: Compare the AI-generated code against this table—page-modification logic lands in content-script.js, event listening lands in service-worker.js—each in its place, and you'll be able to understand the extension's structure.
Checkpoint ✅
- The extension page's Inspect views can open the service worker's DevTools and show its startup log
- The target page shows the content script's changes, with logs in the page's Console
- Can demonstrate: page variables and content script variables are mutually invisible, while the DOM is shared
- Can say which role handles each of the 4 types of requirements
- Knows that after changing a content script, both steps are needed: "reload the extension + refresh the page"
Pitfall reminders
❌ Writing document.querySelector(...) in the service worker
Reason: The service worker cannot access the DOM, so this code is bound to fail.
Correct approach: Leave all page modifications to the content script; the service worker only handles events, and when the page needs to be touched, it instructs the content script to do so via messages.
❌ Calling other extension APIs directly in a content script
Reason: The official docs explicitly state that content scripts are "unable to access other APIs directly"—they can only directly use dom, i18n, storage, and some methods of runtime.
Correct approach: Use runtime.sendMessage() to forward the request to the service worker, which handles it and passes the result back.
❌ Changed the content script but the page shows no change
Reason: Content script changes require both steps—"reload the extension + refresh the host page"; skipping either means you won't see the effect.
Correct approach: First go back to chrome://extensions to reload, then refresh the target page.
❌ Looking for the content script's console.log on the extension page
Reason: The extension page's Errors button only records runtime errors, console.warning, and console.error—ordinary console.log is not recorded; content script logs are in the host page's Console.
Correct approach: Check the web page for content script logs; only service worker logs are in the extension page's Inspect views. If you can't find an error, go back to "The error loop: feed the error message back verbatim" and feed the error to the AI verbatim.
Advanced tips
- Besides static declaration there are two other injection methods: dynamic declaration
chrome.scripting.registerContentScripts(Chrome 96+), and programmatic injectionscripting.executeScript(injected by the extension when needed, e.g. when the toolbar icon is clicked; requires host permissions or temporary grantactiveTab). - The follow-up articles in the official tutorial happen to be organized by role: Run scripts on every page, Inject scripts into the active tab, Handle events with service workers—you can continue directly after finishing this lesson.
- To test the service worker's sleep/wake cycle: open the Application → Service Workers panel in its DevTools and manually start/stop it. Note: Inspecting the service worker keeps it perpetually active—remember to close DevTools after testing.
Lesson summary
✅ Remember the two roles in one sentence:
- service worker = the brain: the extension's event hub, started on demand and stopped when idle, cannot touch the DOM
- content script = the hands and feet: lives inside web pages, can read and modify the page, protected by the isolated world
- Isolated world: JS variables are invisible to each other, while the DOM is shared by both sides
- Log locations: content script → the host web page's Console; service worker → Inspect views on the extension page
Key configuration fields recap
| Field | Recommended value | Description |
|---|---|---|
background.service_worker | "service-worker.js" | Declares the MV3 background script (the MV2 background script is exactly this) |
content_scripts[].matches | Match pattern for the target website | Required; determines which pages get injected |
content_scripts[].js | ["content-script.js"] | List of JS files to inject |
Next lesson preview
One of the two roles' logs is hidden inside the web page, the other inside the extension page—the next lesson 《The extension won't show up: where to find logs and how to read errors》 explains all these locations in one go. You will learn:
- Where errors show up when the manifest is written incorrectly or a permission is misspelled
- Where to look for the logs of the popup, content script, and service worker respectively
- How to use the Errors button on the extension page to quickly pinpoint problems