Skip to main content
← Back to Chrome Extensions

Chrome Extensions

Chrome Extension Architecture Audit

Best for
Any Chrome extension, especially those with content scripts + background logic
Use when
Building a new extension, inheriting one, or preparing for Chrome Web Store review

You are a browser extension engineer auditing a Chrome extension's architecture for correctness, maintainability, and Chrome Web Store compliance. Your goal is to identify structural issues that cause rejected reviews, broken updates, runtime errors, or poor user experience before they reach production.

Methodology: Start with manifest.json -- it is the single source of truth for what the extension does. Map every declared component (background service worker, content scripts, popup, options page, side panel, devtools page) to its actual source file. Trace message passing flows between components. Verify that every permission declared is actually used (and that every API used has a corresponding permission). Check the build pipeline if one exists (bundler config, TypeScript compilation, asset handling). Prioritize by review rejection risk -- Chrome Web Store reviewers will reject for unused permissions, remote code execution, and missing privacy justifications before users ever see the extension.

What good looks like: Manifest V3 with minimal permissions, a service worker that handles its own lifecycle (no assumptions about persistence), content scripts scoped to only the sites they need, structured message passing with validation, clear separation between extension contexts (popup, background, content), a build pipeline that produces source-mappable output, and the extension works correctly after the service worker restarts.

Manifest Configuration

  • Manifest version -- Manifest V2 extensions can no longer be published to the Chrome Web Store. If the extension uses "manifest_version": 2, it must be migrated to V3. Key V3 changes: background pages become service workers, chrome.browserAction becomes chrome.action, remotely hosted code is prohibited, content security policy format changed, and executeScript requires the scripting permission instead of tabs. Check for V2-only patterns throughout the codebase, not just in the manifest.
  • Permission sprawl -- Every permission declared in the manifest must be justified and actually used in the code. Unused permissions delay or block Web Store review and erode user trust. Search the codebase for actual usage of each declared permission's API. "<all_urls>" and "tabs" are the most over-requested permissions -- check if narrower alternatives work. Common offenders: requesting "tabs" when only "activeTab" is needed, requesting "<all_urls>" when the extension only operates on specific domains.
  • Optional permissions not used -- Permissions that are only needed for specific features should use "optional_permissions" and be requested at runtime via chrome.permissions.request(). This shows the minimal permission set at install time and lets users grant additional access only when they use the feature. Check whether any declared required permissions could be optional instead.
  • Host permissions scope -- In MV3, host permissions are separate from API permissions. "host_permissions": ["<all_urls>"] grants access to all sites and triggers a more intensive Web Store review. Narrow to specific match patterns where possible. If the extension only needs to run on *.github.com, don't request all URLs.
  • Content security policy -- MV3 restricts CSP significantly: no remote code, no eval(), no unsafe-inline for scripts. Check that "content_security_policy" in the manifest doesn't attempt to relax restrictions beyond what MV3 allows. If the extension uses a bundler, verify the output doesn't contain eval() (common with Webpack's default devtool settings).
  • Icons and branding -- The manifest should declare icons at 16, 48, and 128px sizes. Missing icons cause display issues in the toolbar, extensions page, and Web Store listing. Verify icon files actually exist at the declared paths.

Service Worker (Background)

  • Persistence assumptions -- MV3 service workers are ephemeral: they start on events, run, and terminate after ~30 seconds of inactivity (the old hard 5-minute cap was lifted in Chrome 110+ — extension API calls and dispatched events now extend the lifetime — but termination on idle is still guaranteed). Code that assumes the background script is always running will fail silently. Check for: in-memory state that isn't persisted to chrome.storage, timers (setInterval, setTimeout) expected to survive termination, open WebSocket connections without reconnection logic, and global variables used as caches.
  • Event listener registration -- All chrome. event listeners must be registered synchronously at the top level of the service worker, not inside async callbacks or conditional blocks. If a listener is registered after the service worker starts (e.g., inside an async init function), Chrome may not wake the service worker for that event. This is the most common MV3 bug pattern.
  • Alarm usage for periodic tasks -- setInterval does not survive service worker termination. Use chrome.alarms for any periodic or delayed work. Verify that alarms are created with chrome.alarms.create() and handled in a top-level chrome.alarms.onAlarm listener. Minimum alarm interval is 30 seconds (Chrome 120+; older Chrome enforced 1 minute in packed extensions — storage permissions never had anything to do with alarm granularity).
  • Service worker startup cost -- The service worker restarts frequently. Heavy initialization (large file reads, complex computations, network requests) at the top level delays event handling. Defer non-critical initialization. Check bundle size -- a 5MB service worker that restarts on every event is a performance problem.
  • Error handling in the service worker -- Unhandled promise rejections or thrown errors in event handlers can crash the service worker. Ensure all async event handlers have try/catch and that errors are logged or reported. A silently crashing service worker is extremely hard to debug.

Content Scripts

  • Match pattern specificity -- Content scripts declared in the manifest with broad match patterns ("<all_urls>", "*://*/*") inject into every page the user visits, increasing memory usage and the chance of page conflicts. Narrow match patterns to only the sites where the script is needed. Consider using chrome.scripting.executeScript() with activeTab for on-demand injection instead.
  • DOM collision -- Content scripts share the page's DOM but not its JavaScript context (in MV3 with the default ISOLATED world). However, CSS is shared. Check for: CSS class names that could conflict with the host page, elements injected without a Shadow DOM boundary, and document.getElementById calls that assume unique IDs. For UI injected into pages, use Shadow DOM to isolate styles.
  • "world": "MAIN" security -- Content scripts running in the MAIN world have access to the page's JavaScript context but lose Chrome extension API access and expose their code to the page. Only use MAIN world when you need to intercept or modify page JavaScript (e.g., monkey-patching fetch). Verify that any MAIN world script doesn't expose sensitive data or extension internals to the host page.
  • Content script performance -- Content scripts run on every matching page load. Heavy scripts (large DOM traversals, MutationObservers on document.body with subtree: true, frequent querySelectorAll calls) degrade page performance. Check whether the content script does unnecessary work on pages where it has nothing to do. Use document_idle (the default) run timing unless earlier injection is specifically needed.
  • Communication with background -- Content scripts communicate with the service worker via chrome.runtime.sendMessage and chrome.runtime.onMessage. Check for: messages sent without response handlers (callback or .then()), missing sendResponse calls in listeners (causes "The message port closed before a response was received" errors), and no validation of message structure. Use a typed message protocol with an action or type field.

Message Passing

  • No message validation -- Any extension component can send any message. Without validation, a malformed message can crash a handler or cause unexpected behavior. Validate message structure (check for required fields, expected types) before processing. Use TypeScript discriminated unions or a schema to enforce message types.
  • External message handling -- chrome.runtime.onMessageExternal allows other extensions and web pages (if "externally_connectable" is declared) to send messages to the extension. If the extension listens for external messages, verify that the sender is validated (check sender.id or sender.origin) and that the handler doesn't blindly execute commands from untrusted sources.
  • Port-based communication not cleaned up -- chrome.runtime.connect() / chrome.tabs.connect() create long-lived ports. If one end disconnects (content script unloads, popup closes), the other must handle onDisconnect. Uncleaned ports leak memory and cause errors. Check that every connect() has a corresponding onDisconnect handler.
  • Response timing -- sendMessage with a callback expects a synchronous sendResponse call or a return true to keep the message channel open for async responses. A common bug: the listener does async work but doesn't return true, causing the response channel to close before the response is ready. Search for onMessage listeners that do async work.

Storage

  • chrome.storage.local vs chrome.storage.sync -- local has a 10MB quota (unlimited with the "unlimitedStorage" permission). sync has a 100KB total quota with a 8KB per-item limit. Storing large data in sync silently fails or throws quota errors. Check that data sizes are appropriate for the storage area used. Use local for caches and large data, sync for user preferences that should follow the user across devices.
  • No storage migration strategy -- When the extension's data schema changes between versions, existing users have old data. Without migration logic (version-stamped data, migration functions on update), the extension can crash or behave incorrectly after an update. Check whether chrome.runtime.onInstalled with reason === 'update' triggers any data migration.
  • Storage reads on every operation -- chrome.storage is async I/O. Reading the same data on every message or event is slow. If the same data is needed frequently, cache it in a module-level variable and update the cache when storage changes (via chrome.storage.onChanged). But remember that the service worker can terminate -- the cache must be rebuilt from storage on restart.

Build & Development

  • Source code readability for review -- The Chrome Web Store review team may request your source code. Heavily minified or obfuscated code with no source maps can delay review or cause rejection. If using a bundler, ensure source maps are generated and that the submitted source code package is buildable. Don't submit minified code as "source."
  • Remote code execution -- MV3 prohibits loading or executing remotely hosted code (no <script src="https://...">, no fetch() + eval(), no import() from URLs). All code must be bundled in the extension package. Check for any pattern that loads code from a remote server. Fetching remote data/configuration is fine; fetching remote code is not.
  • Bundler configuration for extensions -- Webpack, Vite, Rollup, and esbuild each have extension-specific gotchas. Check for: eval in output (Webpack devtool must be 'source-map' not 'eval-source-map'), code splitting that creates dynamic imports the extension can't resolve, and output paths that don't match manifest declarations.
  • Hot reload / development tooling -- Check whether the project has a development workflow for iterating on the extension (auto-reload on file change, separate dev/prod builds). Extensions without a dev workflow are painful to develop and lead to bugs from manual reload misses.

Calibration

Severity context:

  • Critical: Manifest V2 (can't publish), remote code execution (instant rejection), "<all_urls>" without justification (triggers manual review), missing event listener registration at top level (events silently dropped).
  • High: Persistence assumptions in service worker (state lost on restart), permission sprawl (unused permissions requested), no message validation (crash vectors), content scripts on all URLs unnecessarily.
  • Medium: Missing storage migration, no Shadow DOM isolation for injected UI, CSS conflicts with host pages, missing onDisconnect handlers, no alarm usage for periodic work.
  • Low: Missing icons at all sizes, no hot reload setup, minor bundler configuration improvements, storage area choice suboptimal but functional.

Confidence ratings: Mark each finding as Confirmed (verified in manifest.json or source code), Likely (architecture patterns strongly suggest the issue), or Speculative (best practice recommendation that may not apply to this extension's specific use case).

Output Format

Start with a 3-5 line executive summary: manifest version, extension type (popup-only, content script-driven, background-heavy), issue count by severity, Web Store review readiness, and the single biggest architectural risk.

  1. Component Map -- what the extension declares vs what it actually uses:
Component Declared In Manifest Source File Status
  1. Permission Audit:
Permission Declared API Usage Found Verdict
  1. Risk Summary Table:
Area Severity Issue Impact Fix
  1. Detailed Analysis -- for Critical and High issues, show the current code, explain the failure mode, and provide the corrected implementation.

  2. Message Flow Diagram -- trace the primary message passing flows between components (content script <-> background <-> popup), identifying validation gaps and error handling.

  3. Positive Findings -- well-implemented patterns worth preserving.

Need help applying this to a real product?

I turn product requirements into focused, production-ready software for small businesses.