Chrome Extension Extension Architecture — Developer Guide
14 min readChrome Extension Architecture Deep Dive
The Extension Component Model
- How Chrome loads and isolates extension components
- Process model: each component runs in its own context
- Diagram description: Background SW <-> Content Scripts <-> Popup/Options <-> DevTools
Background Service Worker
- Entry point defined in
manifest.json"background": { "service_worker": "background.js" } - Lifecycle: install -> activate -> idle -> terminate -> wake
- No DOM access, no
windowobject - Event-driven: must register listeners at top level
- Persistence: use
chrome.storage(via@theluckystrike/webext-storage) to persist state across restarts - Example:
const storage = createStorage(defineSchema({ lastRun: 'number' }), 'local')
Content Scripts
- Injected into web pages via
manifest.json"content_scripts"orchrome.scripting.executeScript - Isolated world: shares DOM but NOT JavaScript scope with the page
- Can access limited Chrome APIs:
chrome.runtime,chrome.storage - Communication with background: use
@theluckystrike/webext-messaging - Example:
const messenger = createMessenger<MyMessages>(); messenger.sendMessage('getData', { key: 'value' })
Popup and Options Pages
- Popup: triggered by clicking extension icon, lives as long as popup is open
- Options: full page for extension settings, opened via right-click -> Options
- Both have full Chrome API access like background
- State management: use
@theluckystrike/webext-storagewatch()for reactive updates - Example:
storage.watch('theme', (newVal, oldVal) => updateUI(newVal))
DevTools Pages
- Custom panels in Chrome DevTools
- Access to
chrome.devtools.*APIs - Communication pattern: DevTools -> Background -> Content Script
Inter-Component Communication Patterns
- Popup <-> Background: direct
chrome.runtimemessaging - Content <-> Background:
chrome.runtime.sendMessage/chrome.tabs.sendMessage - Using
@theluckystrike/webext-messagingfor type-safe messaging across all components: ```typescript type Messages = { getUser: { request: { id: string }; response: User }; saveData: { request: Data; response: void };Chrome Extension Architecture Patterns
A comprehensive guide to designing scalable, maintainable Chrome extensions using modern architecture patterns. This guide covers foundational structures, state management, cross-context communication, and advanced patterns for building professional-grade extensions.
Table of Contents
- Architecture Models
- Data Flow Patterns
- Code Organization
- Configuration & Features
- Advanced Patterns
- Build System Setup
- References
Architecture Models
Single-Page Popup Architecture
The simplest extension model where all functionality lives in a single popup. Best for utility extensions with limited features.
{
"action": {
"default_popup": "popup.html",
"default_icon": "icon.png"
}
}
Use cases: Clipboard managers, page analyzers, quick toggles.
Multi-Page Extension with Options and Popup
Separates user-facing features into distinct contexts. The popup provides quick actions while the options page handles configuration.
{
"action": {
"default_popup": "popup.html"
},
"options_page": "options.html"
}
Communication between popup and options uses chrome.runtime.sendMessage and chrome.storage.
Sidebar-First Architecture
Uses the side panel API (Manifest V3) as the primary interface. More screen real estate than popups, persists while browsing.
{
"side_panel": {
"default_path": "sidepanel.html"
}
}
Best for: Note-taking, reading tools, productivity boosters.
Content Script Overlay Architecture
Content scripts act as the primary UI, overlaying elements on the page. Useful for page-specific enhancements.
// content.js - Inject overlay when page loads
const overlay = document.createElement('div');
overlay.id = 'my-extension-overlay';
overlay.innerHTML = '<div class="panel">...</div>';
document.body.appendChild(overlay);
Background Processing Architecture
Service workers handle long-running tasks, periodic sync, and cross-tab coordination. The UI remains lightweight.
// background.js
chrome.alarms.create('sync', { periodInMinutes: 15 });
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'sync') {
performBackgroundSync();
}
});
Data Flow Patterns
Event-Driven vs Polling Patterns
Event-driven (recommended): Use Chrome’s built-in events for responsiveness and efficiency.
// Event-driven: Listen for tab updates
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete') {
handlePageLoad(tabId);
}
});
Polling (avoid unless necessary): Use setInterval only when events aren’t available.
// Polling - use sparingly
setInterval(() => {
checkExternalState();
}, 60000);
State Management Patterns
Extensions require state synchronization across multiple contexts. Choose based on complexity:
- Local State: Simple extensions with isolated features
- Shared State via Storage: Mid-complexity extensions
- Centralized Store: Complex applications
Centralized Store in Service Worker
The service worker acts as the single source of truth, managing state for all contexts.
// background.js - Centralized store
class ExtensionStore {
constructor() {
this.state = {};
this.listeners = new Set();
this.loadState();
}
async loadState() {
const result = await chrome.storage.local.get(null);
this.state = result;
this.notifyListeners();
}
setState(patch) {
this.state = { ...this.state, ...patch };
chrome.storage.local.set(patch);
this.notifyListeners(patch);
}
subscribe(listener) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
notifyListeners(patch) {
this.listeners.forEach(listener => listener(this.state, patch));
}
}
const store = new ExtensionStore();
Reactive UI Updates from Storage Changes
All contexts can subscribe to storage changes for real-time updates.
// popup.js - React to storage changes
chrome.storage.onChanged.addListener((changes, area) => {
if (area === 'local' && changes.settings) {
updateUI(changes.settings.newValue);
}
});
Code Organization
Module Organization for Large Extensions
Structure by feature rather than by file type for better maintainability.
src/
├── features/
│ ├── bookmark-manager/
│ │ ├── bookmark-manager.ts
│ │ ├── BookmarkList.tsx
│ │ └── bookmark-manager.test.ts
│ └── note-taking/
│ ├── note-taking.ts
│ └── NoteEditor.tsx
├── shared/
│ ├── storage/
│ ├── i18n/
│ └── utils/
└── background/
└── index.ts
Shared Utilities Between Contexts
Use a shared module bundled separately for code used across contexts.
// shared/utils.js - Build target for all contexts
export function formatDate(date) {
return new Intl.DateTimeFormat().format(date);
}
export function debounce(fn, delay) {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
}
Manifest.json as the Blueprint
- Structure overview: manifest_version, name, version, permissions, background, content_scripts, action
- How Chrome reads the manifest to wire up components
- Common mistakes: missing permissions, wrong paths, invalid JSON
Security Boundaries
- Content scripts can’t access extension pages directly
- Web pages can’t access extension APIs
- Extension pages can’t access other extensions
- CSP restrictions in MV3 (cross-ref:
docs/mv3/content-security-policy.md)
Related Articles
Related Articles
Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.
Dependency Injection Patterns
Essential for testing and mocking Chrome APIs.
//.di/container.ts
class DIContainer {
constructor() {
this.services = new Map();
}
register(key, factory) {
this.services.set(key, factory(this));
}
resolve(key) {
const factory = this.services.get(key);
if (!factory) throw new Error(`Service ${key} not found`);
return factory(this);
}
}
// Register Chrome API wrapper
container.register('chromeStorage', () => ({
get: (keys) => chrome.storage.local.get(keys),
set: (items) => chrome.storage.local.set(items),
}));
Plugin/Middleware Architecture
Extend functionality without modifying core code.
// core/extension.ts
class ExtensionCore {
constructor() {
this.middlewares = [];
}
use(middleware) {
this.middlewares.push(middleware);
}
async process(action) {
let result = action;
for (const middleware of this.middlewares) {
result = await middleware(result) || result;
}
return result;
}
}
// Middleware example
const loggingMiddleware = async (action) => {
console.log(`[${action.type}]`, action.payload);
return action;
};
Configuration & Features
Configuration-Driven Behavior
Externalize behavior to configuration for flexibility.
{
"featureFlags": {
"darkMode": true,
"betaFeatures": false
},
"contentScriptConfig": {
"targetSites": ["*.github.com", "*.example.com"]
}
}
// Load configuration
async function getFeatureConfig() {
const config = await chrome.storage.local.get('featureFlags');
return config.featureFlags || {};
}
Feature Flag Architecture
Roll out features gradually and enable testing.
// feature-flags.ts
export class FeatureFlags {
constructor() {
this.flags = {};
this.load();
}
async load() {
const result = await chrome.storage.local.get('featureFlags');
this.flags = result.featureFlags || {};
}
isEnabled(flag) {
return this.flags[flag] === true;
}
async enable(flag) {
this.flags[flag] = true;
await chrome.storage.local.set({ featureFlags: this.flags });
}
}
Advanced Patterns
Multi-Extension Communication
Extensions can communicate via shared storage and messaging.
// Extension A - sends message
chrome.runtime.sendMessage(
'extensionBId',
{ type: 'SHARE_DATA', payload: data },
(response) => console.log(response)
);
// Extension B - receives message
chrome.runtime.onMessageExternal.addListener(
(message, sender, sendResponse) => {
if (message.type === 'SHARE_DATA') {
handleSharedData(message.payload);
sendResponse({ success: true });
}
}
);
Extension Suite Architecture
Multiple related extensions sharing code via internal package.
packages/
├── shared/ # Common utilities
├── core/ # Core extension logic
├── extension-a/ # Extension A
└── extension-b/ # Extension B
{
"name": "extension-suite",
"workspaces": ["packages/*"]
}
Monorepo Structure for Extensions
Manage multiple extensions in one repository.
my-extensions/
├── package.json
├── turbo.json
├── apps/
│ ├── my-extension/
│ │ ├── manifest.json
│ │ └── src/
│ └── my-second-extension/
│ ├── manifest.json
│ └── src/
└── packages/
└── shared-utils/
Build System Setup
Webpack Configuration
// webpack.config.js
const path = require('path');
module.exports = [
{
entry: './src/popup/index.js',
output: {
path: path.resolve(__dirname, 'dist/popup'),
filename: 'popup.js',
},
target: 'web',
},
{
entry: './src/background/index.js',
output: {
path: path.resolve(__dirname, 'dist/background'),
filename: 'background.js',
},
target: 'webworker',
},
{
entry: './src/content/index.js',
output: {
path: path.resolve(__dirname, 'dist/content'),
filename: 'content.js',
},
target: 'web',
},
];
Vite Configuration
// vite.config.ts
import { defineConfig } from 'vite';
import { resolve } from 'path';
export defineConfig({
build: {
rollupOptions: {
input: {
popup: resolve(__dirname, 'popup.html'),
options: resolve(__dirname, 'options.html'),
background: resolve(__dirname, 'src/background/index.ts'),
},
},
},
});
Tsup Configuration
// tsup.config.ts
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/background/index.ts', 'src/content/index.ts'],
format: ['esm', 'cjs'],
dts: true,
splitting: false,
sourcemap: true,
});
References
- Chrome Extensions Documentation
- Chrome Extensions API Reference
- Manifest V3 Migration Guide
- Chrome Web Store Publishing
Last updated: 2025. For the latest patterns and best practices, refer to the official Chrome extensions documentation.