Chrome Extension Extension Architecture — Developer Guide

14 min read

Chrome Extension Architecture Deep Dive

The Extension Component Model

Background Service Worker

Content Scripts

DevTools Pages

Inter-Component Communication 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

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.

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:

  1. Local State: Simple extensions with isolated features
  2. Shared State via Storage: Mid-complexity extensions
  3. 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

Security Boundaries

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


Last updated: 2025. For the latest patterns and best practices, refer to the official Chrome extensions documentation.