Understanding Chrome Extensions: A Developer's Guide to Manifest V3
Chrome extensions have become essential tools that enhance browser functionality for millions of users worldwide. As a developer, understanding how they work under the hood is crucial for building powerful, secure, and performant extensions. In this comprehensive guide, we'll dive deep into Chrome extension architecture, the Manifest V3 specification, and everything you need to know to publish your extension.
ποΈ Extension Architecture: The Core Components
Every Chrome extension is built from several key components that work together seamlessly:
1. Manifest File (manifest.json)
The manifest is the blueprint of your extension - a JSON configuration file that must reside in the root directory. It's the first thing Chrome reads and contains metadata, permissions, and component declarations.
Required fields:
{
"manifest_version": 3,
"name": "My Extension",
"version": "1.0.0"
}
Common additional fields:
{
"manifest_version": 3,
"name": "My Awesome Extension",
"version": "1.0.0",
"description": "A brief description of what your extension does",
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"action": {
"default_popup": "popup.html",
"default_icon": "icons/icon48.png",
"default_title": "Click me!"
},
"background": {
"service_worker": "background.js",
"type": "module"
},
"content_scripts": [
{
"matches": ["https://*/*", "http://*/*"],
"js": ["content.js"],
"css": ["styles.css"]
}
],
"permissions": [
"storage",
"tabs",
"activeTab"
],
"host_permissions": [
"https://api.example.com/*"
]
}
2. Service Worker (Background Script)
In Manifest V3, the persistent background page has been replaced with service workers. This is a major architectural change designed to improve performance and security.
Key characteristics:
- Runs in the background only when needed (event-driven)
- No access to the DOM
- Automatically terminates after 30 seconds of inactivity
- Restarts when an event is triggered
Example service worker:
// background.js
chrome.runtime.onInstalled.addListener(() => {
console.log('Extension installed');
chrome.storage.local.set({ initialized: true });
});
// Listen for tab updates
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete') {
console.log('Tab loaded:', tab.url);
}
});
// Listen for messages
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'getData') {
// Process data
sendResponse({ data: 'Response from background' });
}
return true; // Required for async sendResponse
});
3. Content Scripts
Content scripts run in the context of web pages and can interact with the DOM. They're injected into pages matching patterns defined in the manifest.
Two ways to inject content scripts:
Declarative (via manifest):
{
"content_scripts": [
{
"matches": ["https://github.com/*"],
"js": ["content.js"],
"run_at": "document_idle"
}
]
}
Programmatic (via scripting API):
// In service worker
chrome.action.onClicked.addListener((tab) => {
chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ['content.js']
});
});
// Or inject a function
function injectedFunction() {
document.body.style.backgroundColor = 'lightblue';
}
chrome.scripting.executeScript({
target: { tabId: tab.id },
func: injectedFunction
});
4. Popup / Action UI
The popup is an HTML page that appears when users click your extension icon in the toolbar.
<!-- popup.html -->
<!DOCTYPE html>
<html>
<head>
<style>
body { width: 300px; padding: 10px; }
</style>
</head>
<body>
<h1>My Extension</h1>
<button id="actionBtn">Do Something</button>
<script src="popup.js"></script>
</body>
</html>
// popup.js
document.getElementById('actionBtn').addEventListener('click', async () => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
chrome.tabs.sendMessage(tab.id, {
action: 'performAction'
}, (response) => {
console.log('Response:', response);
});
});
π‘ Communication Patterns
One of the most critical aspects of extension development is understanding how different components communicate with each other.
1. One-time Messages
From content script to service worker:
// content.js
chrome.runtime.sendMessage(
{ greeting: 'hello', data: 'some data' },
(response) => {
console.log('Response:', response);
}
);
From service worker to content script:
// background.js
chrome.tabs.sendMessage(
tabId,
{ action: 'updateUI', data: 'new data' },
(response) => {
console.log('Response:', response);
}
);
From popup to service worker:
// popup.js
chrome.runtime.sendMessage(
{ request: 'getData' },
(response) => {
console.log('Data:', response.data);
}
);
2. Long-lived Connections
For multiple messages between components, use ports:
// content.js
const port = chrome.runtime.connect({ name: 'my-channel' });
port.postMessage({ type: 'init' });
port.onMessage.addListener((msg) => {
console.log('Received:', msg);
});
// background.js
chrome.runtime.onConnect.addListener((port) => {
console.log('Connected:', port.name);
port.onMessage.addListener((msg) => {
console.log('Message received:', msg);
port.postMessage({ response: 'acknowledged' });
});
});
3. Communication with Web Pages
Content scripts and web pages share the DOM but have isolated JavaScript contexts. Use window.postMessage
:
// content.js
window.addEventListener('message', (event) => {
if (event.source !== window) return;
if (event.data.type === 'FROM_PAGE') {
console.log('Message from page:', event.data.text);
// Forward to service worker if needed
chrome.runtime.sendMessage(event.data);
}
});
// webpage.js
window.postMessage(
{ type: 'FROM_PAGE', text: 'Hello from webpage!' },
'*'
);
π Permissions System
Permissions in Manifest V3 are more granular and security-focused. There are four types:
1. Regular Permissions
API permissions that don't require host access:
{
"permissions": [
"storage", // chrome.storage API
"tabs", // Limited tabs API access
"activeTab", // Access to active tab (requires user gesture)
"contextMenus", // Create context menu items
"notifications", // Show notifications
"scripting" // Execute scripts programmatically
]
}
2. Host Permissions
Access to specific URLs or patterns:
{
"host_permissions": [
"https://api.example.com/*",
"https://*.google.com/*",
"<all_urls>" // Access to all URLs (use sparingly!)
]
}
3. Optional Permissions
Requested at runtime rather than install time:
{
"optional_permissions": ["downloads", "bookmarks"],
"optional_host_permissions": ["https://*/*"]
}
Request them in your code:
document.getElementById('enableFeature').addEventListener('click', async () => {
const granted = await chrome.permissions.request({
permissions: ['downloads'],
origins: ['https://example.com/*']
});
if (granted) {
console.log('Permission granted!');
}
});
4. Content Script Matches
Separate from host_permissions - controls where content scripts inject:
{
"content_scripts": [
{
"matches": ["https://github.com/*"],
"js": ["content.js"]
}
]
}
β‘ Manifest V3 Key Changes
If you're migrating from V2 or starting fresh, here are the critical changes:
Background Pages β Service Workers
// β Manifest V2
{
"background": {
"scripts": ["background.js"],
"persistent": false
}
}
// β
Manifest V3
{
"background": {
"service_worker": "background.js",
"type": "module" // Optional: enables ES modules
}
}
Host Permissions Separation
// β Manifest V2
{
"permissions": [
"tabs",
"https://*/*"
]
}
// β
Manifest V3
{
"permissions": ["tabs"],
"host_permissions": ["https://*/*"]
}
executeScript Migration
// β Manifest V2
chrome.tabs.executeScript(tabId, {
code: 'document.body.style.backgroundColor = "red"'
});
// β
Manifest V3
chrome.scripting.executeScript({
target: { tabId: tabId },
func: () => {
document.body.style.backgroundColor = "red";
}
});
webRequest β declarativeNetRequest
For content blocking and request modification:
{
"permissions": ["declarativeNetRequest"],
"declarative_net_request": {
"rule_resources": [{
"id": "ruleset_1",
"enabled": true,
"path": "rules.json"
}]
}
}
No Remote Code
All code must be bundled with the extension. External scripts from CDNs are no longer allowed for security reasons.
π¦ What You Can Do Without Permissions
Some Chrome APIs don't require permissions:
-
chrome.runtime
(messaging, extension info) -
chrome.i18n
(internationalization) -
chrome.action
(basic toolbar icon interaction) -
window
anddocument
APIs in content scripts - Standard web APIs (fetch, localStorage in content scripts)
π Publishing to Chrome Web Store
Step 1: Prepare Your Extension
-
Test thoroughly - Load unpacked in
chrome://extensions/
-
Create assets:
- 128x128 icon (required)
- 440x280 or 920x680 screenshots (required)
- 1280x800 promotional image (optional)
- Privacy policy (if handling user data)
Package your extension:
# Create a zip file with manifest.json at root
zip -r extension.zip . -x "*.git*" "node_modules/*" ".DS_Store"
Step 2: Register as Developer
- Go to Chrome Web Store Developer Dashboard
- Pay $5 one-time registration fee
- Enable 2-factor authentication (required as of 2024)
Step 3: Upload Extension
- Click "New Item"
- Upload your zip file
-
Fill out the Store Listing:
- Description - Clear explanation of functionality
- Category - Choose appropriate category
- Language - Target language(s)
- Screenshots - At least 1, up to 5
-
Privacy Tab:
- Single Purpose - Declare extension's purpose
- Permission Justifications - Explain why each permission is needed
- Data Usage - Disclose any data collection
-
Distribution Tab:
- Visibility - Public, Unlisted, or Private
- Regions - Select target countries
- Pricing - Free or paid
Step 4: Submit for Review
Review times vary but most extensions are reviewed within 3 days. You'll receive an email notification once reviewed.
Publishing Options:
Public: Available to everyone in Chrome Web Store
Unlisted: Only accessible via direct link
Private: Only for users in your domain/organization
Step 5: Update Your Extension
To publish updates:
- Increment version number in manifest.json
- Upload new zip file
- Submit for review
- Can use percentage rollout to gradually release to users
Programmatic Publishing (CI/CD)
You can automate publishing using the Chrome Web Store API:
// Using chrome-webstore-upload npm package
import chromeWebstoreUpload from 'chrome-webstore-upload';
const store = chromeWebstoreUpload({
extensionId: 'your-extension-id',
clientId: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
refreshToken: process.env.REFRESH_TOKEN
});
const myZipFile = fs.createReadStream('./extension.zip');
await store.uploadExisting(myZipFile);
await store.publish();
π οΈ Development Tips & Best Practices
-
Use ES Modules - Set
"type": "module"
in background config -
Handle Service Worker Lifecycle - Store state in
chrome.storage
, not variables - Minimize Permissions - Only request what you absolutely need
- Test on Multiple Chrome Versions - Use beta/dev channels
- Monitor Performance - Service workers should be lightweight
-
Implement Error Handling - Always check
chrome.runtime.lastError
-
Use Promises - Modern Chrome APIs support promises in the
chrome.*
namespace
// Modern promise-based approach
const tabs = await chrome.tabs.query({ active: true });
const result = await chrome.storage.local.get('key');
π― Common Pitfalls to Avoid
- β Using
localStorage
in service workers (not available) - β Assuming service worker stays alive indefinitely
- β Not returning
true
fromonMessage
listener for async responses - β Requesting
<all_urls>
without justification - β Including unnecessary files in your package
- β Not handling permission denials gracefully
- β Using
eval()
or inline scripts (CSP violation)
π¬ Conclusion
Chrome extensions are powerful tools that can significantly enhance user experience. With Manifest V3, Google has made extensions more secure, performant, and privacy-focused. While the migration from V2 requires some changes, the improved architecture leads to better extensions overall.
π Resources
- Chrome Extensions Documentation
- Manifest V3 Migration Guide
- Chrome Web Store Developer Dashboard
- Extension Samples
About Me: I've developed multiple complex Chrome extensions over the years, ranging from productivity tools to advanced web automation solutions. Through this journey, I've learned the intricacies of extension architecture, performance optimization, and the evolving Chrome extension ecosystem. If you have questions about Chrome extension development, feel free to reach out!
My site >> https://justinbrowser.com/
Happy coding! π
Top comments (0)