
Chrome Extension
- 643 installs
- 186 repo stars
- Updated July 24, 2026
- pproenca/dot-skills
chrome-extension is an agent skill that encodes 40+ Manifest V3 performance rules across 8 categories so developers who build or maintain Chrome extensions get consistent, optimized code from AI-assisted workflows.
About
chrome-extension is a Chrome Developer Relations–authored agent skill (version 0.1.0, January 2026) that teaches LLMs how to generate, maintain, and refactor Chrome Manifest V3 extension codebases. The skill bundles 40+ prioritized performance rules across 8 categories—from critical service worker lifecycle and content script optimization to incremental API usage patterns—with explanations and real-world examples for each rule. Agents apply the guidance when scaffolding new extensions, auditing existing MV3 projects, or refactoring legacy Manifest V2 patterns. Developers reach for chrome-extension when AI-generated extension code drifts from Chrome best practices or when they need automated consistency across popup, background, and content-script layers without manually re-reading Chrome extension documentation.
- 40+ prioritized rules across 8 categories for Manifest V3 performance
- Critical focus on service worker lifecycle, content scripts, and state persistence
- Side-by-side incorrect vs correct code examples with impact metrics
- Optimized for AI agents and LLMs to automate refactoring and code generation
- Reduces memory usage by 50-100MB per idle extension through correct patterns
Chrome Extension by the numbers
- 643 all-time installs (skills.sh)
- Ranked #1,489 of 16,565 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
npx skills add https://github.com/pproenca/dot-skills --skill chrome-extensionAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 643 |
|---|---|
| repo stars | ★ 186 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 24, 2026 |
| Repository | pproenca/dot-skills ↗ |
How do you optimize Chrome Manifest V3 extensions with AI?
Generate, maintain, and optimize Chrome Manifest V3 extension code with AI agents that follow 40+ performance rules.
Who is it for?
Developers building or maintaining Chrome Manifest V3 extensions who want AI agents to follow Chrome Developer Relations performance guidance automatically.
Skip if: Developers building Firefox add-ons, Safari Web Extensions-only projects, or non-browser web apps that do not use the Chrome extension platform.
When should I use this skill?
User asks to create, refactor, audit, or optimize a Chrome extension, mentions Manifest V3, service workers, or content scripts.
What you get
Manifest V3 extension code aligned to 40+ performance rules, optimized service worker lifecycle, and refactored content script patterns.
- Optimized MV3 extension code
- Refactored service worker and content script files
By the numbers
- 40+ performance rules across 8 categories
- Version 0.1.0 (January 2026)
Files
Chrome Extension Best Practices
Comprehensive performance and code quality guide for Chrome Extensions (Manifest V3). Contains 67 rules across 12 categories, prioritized by impact to guide automated refactoring and code generation.
When to Apply
Reference these guidelines when:
- Writing new Chrome extension code
- Migrating from Manifest V2 to Manifest V3
- Optimizing service worker lifecycle and state management
- Implementing content scripts for page interaction
- Debugging performance issues in extensions
Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Service Worker Lifecycle | CRITICAL | sw- |
| 2 | Content Script Optimization | CRITICAL | content- |
| 3 | Message Passing Efficiency | HIGH | msg- |
| 4 | Storage Operations | HIGH | storage- |
| 5 | Network & Permissions | MEDIUM-HIGH | net- |
| 6 | Memory Management | MEDIUM | mem- |
| 7 | UI Performance | MEDIUM | ui- |
| 8 | API Usage Patterns | LOW-MEDIUM | api- |
| 9 | Code Style & Naming | MEDIUM | style- |
| 10 | Component Patterns | MEDIUM | comp- |
| 11 | Error Handling | HIGH | err- |
| 12 | Testing Patterns | MEDIUM | test- |
Quick Reference
1. Service Worker Lifecycle (CRITICAL)
- `sw-persist-state-storage` - Persist state with chrome.storage instead of global variables
- `sw-avoid-keepalive` - Avoid artificial service worker keep-alive patterns
- `sw-use-alarms-api` - Use chrome.alarms instead of setTimeout/setInterval
- `sw-return-true-async` - Return true from message listeners for async responses
- `sw-register-listeners-toplevel` - Register event listeners at top level
- `sw-use-offscreen-for-dom` - Use offscreen documents for DOM APIs
2. Content Script Optimization (CRITICAL)
- `content-use-specific-matches` - Use specific URL match patterns
- `content-use-document-idle` - Use document_idle for content script injection
- `content-programmatic-injection` - Prefer programmatic injection over manifest declaration
- `content-minimize-script-size` - Minimize content script bundle size
- `content-batch-dom-operations` - Batch DOM operations to minimize reflows
- `content-use-mutation-observer` - Use MutationObserver instead of polling
3. Message Passing Efficiency (HIGH)
- `msg-use-ports-for-frequent` - Use port connections for frequent message exchange
- `msg-minimize-payload-size` - Minimize message payload size
- `msg-debounce-frequent-events` - Debounce high-frequency events before messaging
- `msg-check-lasterror` - Always check chrome.runtime.lastError
- `msg-avoid-broadcast-to-all-tabs` - Avoid broadcasting messages to all tabs
4. Storage Operations (HIGH)
- `storage-batch-operations` - Batch storage operations instead of individual calls
- `storage-choose-correct-type` - Choose the correct storage type for your use case
- `storage-cache-frequently-accessed` - Cache frequently accessed storage values
- `storage-use-session-for-temp` - Use storage.session for temporary runtime data
- `storage-avoid-storing-large-blobs` - Avoid storing large binary blobs
5. Network & Permissions (MEDIUM-HIGH)
- `net-use-declarativenetrequest` - Use declarativeNetRequest instead of webRequest
- `net-request-minimal-permissions` - Request minimal required permissions
- `net-use-activetab` - Use activeTab permission instead of broad host permissions
- `net-limit-csp-modifications` - Avoid modifying Content Security Policy headers
6. Memory Management (MEDIUM)
- `mem-cleanup-event-listeners` - Clean up event listeners when content script unloads
- `mem-avoid-detached-dom` - Avoid holding references to detached DOM nodes
- `mem-avoid-closure-leaks` - Avoid accidental closure memory leaks
- `mem-clear-intervals-timeouts` - Clear intervals and timeouts on cleanup
- `mem-use-weak-collections` - Use WeakMap and WeakSet for DOM element references
7. UI Performance (MEDIUM)
- `ui-minimize-popup-bundle` - Minimize popup bundle size for fast startup
- `ui-render-with-cached-data` - Render popup UI with cached data first
- `ui-batch-badge-updates` - Batch badge updates to avoid flicker
- `ui-use-options-page-lazy` - Lazy load options page sections
8. API Usage Patterns (LOW-MEDIUM)
- `api-use-promises-over-callbacks` - Use promise-based API calls over callbacks
- `api-query-tabs-efficiently` - Query tabs with specific filters
- `api-avoid-redundant-api-calls` - Avoid redundant API calls in loops
- `api-use-alarms-minperiod` - Respect alarms API minimum period
- `api-handle-context-invalidated` - Handle extension context invalidated errors
- `api-use-declarative-content` - Use declarative content API for page actions
9. Code Style & Naming (MEDIUM)
- `style-boolean-naming` - Use is/has/should prefixes for boolean variables
- `style-cache-naming` - Use consistent cache variable naming
- `style-constants` - Define constants for magic values
- `style-directory-structure` - Organize code by feature/layer
- `style-file-naming` - Use consistent file naming conventions
- `style-function-naming` - Use descriptive function names
- `style-import-type` - Use type-only imports for types
- `style-index-entry-points` - Use index files for module entry points
- `style-message-enums` - Use enums for message types
- `style-type-naming` - Use PascalCase for types and interfaces
10. Component Patterns (MEDIUM)
- `comp-adapter-interface` - Use adapter pattern for browser APIs
- `comp-content-script-structure` - Structure content scripts consistently
- `comp-css-class-patterns` - Use BEM or prefixed CSS classes
- `comp-manager-class` - Use manager classes for complex state
- `comp-type-guards` - Use type guards for runtime validation
- `comp-ui-components` - Create reusable UI components
11. Error Handling (HIGH)
- `err-context-invalidation` - Handle extension context invalidation
- `err-early-return` - Use early returns for error handling
- `err-null-coalescing` - Use nullish coalescing for defaults
- `err-promise-barrier` - Use promise barriers for coordination
- `err-storage-operations` - Handle storage operation failures
- `err-url-parsing` - Safely parse URLs with try/catch
- `err-validation-pattern` - Validate inputs at boundaries
12. Testing Patterns (MEDIUM)
- `test-browser-api-mocking` - Mock chrome APIs in tests
- `test-organization` - Organize tests by feature
- `test-validation-functions` - Test validation functions thoroughly
How to Use
Read individual reference files for detailed explanations and code examples:
- Section definitions - Category structure and impact levels
- Rule template - Template for adding new rules
Full Compiled Document
For a complete guide with all rules in a single document, see AGENTS.md.
Reference Files
| File | Description |
|---|---|
| AGENTS.md | Complete compiled guide with all rules |
| references/_sections.md | Category definitions and ordering |
| assets/templates/_template.md | Template for new rules |
| metadata.json | Version and reference information |
Chrome Extensions
Version 0.1.0 Chrome Developer Relations January 2026
Note:
This document is mainly for agents and LLMs to follow when maintaining,
generating, or refactoring codebases. Humans may also find it useful,
but guidance here is optimized for automation and consistency by AI-assisted workflows.
---
Abstract
Comprehensive performance optimization guide for Chrome Extensions (Manifest V3), designed for AI agents and LLMs. Contains 40+ rules across 8 categories, prioritized by impact from critical (service worker lifecycle, content script optimization) to incremental (API usage patterns). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.
---
Table of Contents
1. Service Worker Lifecycle — CRITICAL
- 1.1 Avoid Artificial Service Worker Keep-Alive Patterns — CRITICAL (reduces memory usage by 50-100MB per idle extension)
- 1.2 Persist State with chrome.storage Instead of Global Variables — CRITICAL (prevents complete state loss on SW termination)
- 1.3 Register Event Listeners at Top Level of Service Worker — CRITICAL (prevents missed events when SW restarts)
- 1.4 Return true from Message Listeners for Async Responses — CRITICAL (prevents undefined responses and message channel closure)
- 1.5 Use chrome.alarms Instead of setTimeout/setInterval — CRITICAL (prevents timer callbacks from being lost on SW termination)
- 1.6 Use Offscreen Documents for DOM APIs — CRITICAL (enables DOM manipulation without content script injection)
2. Content Script Optimization — CRITICAL
- 2.1 Batch DOM Operations to Minimize Reflows — CRITICAL (reduces layout thrashing by 10-100×)
- 2.2 Minimize Content Script Bundle Size — CRITICAL (reduces page load impact by 100-500ms per page)
- 2.3 Prefer Programmatic Injection Over Manifest Declaration — CRITICAL (loads scripts only when user invokes feature)
- 2.4 Use document_idle for Content Script Injection — CRITICAL (eliminates page load blocking, faster initial render)
- 2.5 Use MutationObserver Instead of Polling for DOM Changes — CRITICAL (eliminates polling overhead, event-driven detection)
- 2.6 Use Specific URL Match Patterns Instead of All URLs — CRITICAL (reduces script injection by 90%+, faster browsing)
3. Message Passing Efficiency — HIGH
- 3.1 Always Check chrome.runtime.lastError in Callbacks — HIGH (prevents silent failures and memory leaks)
- 3.2 Avoid Broadcasting Messages to All Tabs — HIGH (reduces message overhead from O(n) to O(1))
- 3.3 Debounce High-Frequency Events Before Messaging — HIGH (reduces message volume by 90%+ for scroll/resize events)
- 3.4 Minimize Message Payload Size — HIGH (reduces serialization overhead by 2-10×)
- 3.5 Use Port Connections for Frequent Message Exchange — HIGH (reduces messaging overhead by 50-80% for repeated messages)
4. Storage Operations — HIGH
- 4.1 Avoid Storing Large Binary Blobs in chrome.storage — HIGH (prevents quota exhaustion and serialization overhead)
- 4.2 Batch Storage Operations Instead of Individual Calls — HIGH (reduces storage overhead by 5-20× for multiple values)
- 4.3 Cache Frequently Accessed Storage Values in Memory — HIGH (eliminates repeated async storage reads)
- 4.4 Choose the Correct Storage Type for Your Use Case — HIGH (prevents quota errors and sync throttling)
- 4.5 Use storage.session for Temporary Runtime Data — HIGH (auto-cleanup on browser close, faster access)
5. Network & Permissions — MEDIUM-HIGH
- 5.1 Avoid Modifying Content Security Policy Headers — MEDIUM-HIGH (prevents security degradation and site breakage)
- 5.2 Request Minimal Required Permissions — MEDIUM-HIGH (reduces permission warnings, higher install rates)
- 5.3 Use activeTab Permission Instead of Broad Host Permissions — MEDIUM-HIGH (eliminates permission warning, 0 scary prompts)
- 5.4 Use declarativeNetRequest Instead of webRequest for Blocking — MEDIUM-HIGH (eliminates request interception latency, lower memory usage)
6. Memory Management — MEDIUM
- 6.1 Avoid Accidental Closure Memory Leaks — MEDIUM (prevents large objects from being retained unexpectedly)
- 6.2 Avoid Holding References to Detached DOM Nodes — MEDIUM (prevents DOM trees from being garbage collected)
- 6.3 Clean Up Event Listeners When Content Script Unloads — MEDIUM (prevents memory accumulation on long-running tabs)
- 6.4 Clear Intervals and Timeouts on Cleanup — MEDIUM (prevents orphaned timers from running after context destroyed)
- 6.5 Use WeakMap and WeakSet for DOM Element References — MEDIUM (allows automatic garbage collection of cached elements)
7. UI Performance — MEDIUM
- 7.1 Batch Badge Updates to Avoid Flicker — MEDIUM (prevents visual flicker and reduces API calls)
- 7.2 Lazy Load Options Page Sections — MEDIUM (faster initial options page load)
- 7.3 Minimize Popup Bundle Size for Fast Startup — MEDIUM (reduces popup open time by 100-500ms)
- 7.4 Render Popup UI with Cached Data First — MEDIUM (eliminates loading spinners, instant perceived load)
8. API Usage Patterns — LOW-MEDIUM
- 8.1 Avoid Redundant API Calls in Loops — LOW-MEDIUM (reduces API overhead from N calls to 1)
- 8.2 Handle Extension Context Invalidated Errors — LOW-MEDIUM (prevents errors after extension update or reload)
- 8.3 Query Tabs with Specific Filters — LOW-MEDIUM (reduces processing from all tabs to relevant subset)
- 8.4 Respect Alarms API Minimum Period — LOW-MEDIUM (prevents unexpected 1-minute rounding)
- 8.5 Use Declarative Content API for Page Actions — LOW-MEDIUM (reduces service worker wake-ups for icon state changes)
- 8.6 Use Promise-Based API Calls Over Callbacks — LOW-MEDIUM (reduces callback nesting by 3-5 levels)
---
References
1. https://developer.chrome.com/docs/extensions/ 2. https://developer.chrome.com/docs/extensions/develop/migrate 3. https://developer.chrome.com/docs/extensions/develop/concepts/service-workers/lifecycle 4. https://developer.chrome.com/docs/extensions/develop/concepts/content-scripts 5. https://developer.chrome.com/docs/extensions/reference/api/storage 6. https://developer.chrome.com/docs/extensions/develop/concepts/messaging 7. https://developer.chrome.com/blog/longer-esw-lifetimes 8. https://developer.chrome.com/blog/Offscreen-Documents-in-Manifest-v3
---
Source Files
This document was compiled from individual reference files. For detailed editing or extension:
| File | Description |
|---|---|
| references/_sections.md | Category definitions and impact ordering |
| assets/templates/_template.md | Template for creating new rules |
| SKILL.md | Quick reference entry point |
| metadata.json | Version and reference URLs |
Rule Title Here
Brief explanation of WHY this matters (1-3 sentences). Focus on the performance implications and user impact.
Incorrect (description of what's wrong):
// Bad code example with comments on key lines
// Example: Reads storage on every iteration
async function processItems(items) {
for (const item of items) {
const { settings } = await chrome.storage.local.get('settings'); // N reads
applySettings(item, settings);
}
}Correct (description of the improvement):
// Good code example with minimal changes from incorrect
// Example: Single read, reused for all items
async function processItems(items) {
const { settings } = await chrome.storage.local.get('settings'); // 1 read
for (const item of items) {
applySettings(item, settings);
}
}Alternative (when applicable):
// Alternative approach for specific contextsWhen NOT to use this pattern:
- Exception scenario 1
- Exception scenario 2
Reference: Reference Title
{
"name": "chrome-extension",
"description": "Chrome Extensions (Manifest V3) performance and code quality guidelines including TypeScript patterns and testing",
"version": "1.0.5",
"organization": "Chrome Developer Relations",
"technology": "Chrome Extensions",
"date": "January 2026",
"tags": [
"chrome",
"extension",
"manifest-v3",
"service-worker",
"typescript",
"code-style",
"testing"
],
"totalRules": 67,
"abstract": "Comprehensive performance and code quality guide for Chrome Extensions (Manifest V3). Contains 67 rules across 12 categories, prioritized by impact from critical (service worker lifecycle, content script optimization) to incremental (API usage patterns). Includes TypeScript patterns, error handling, and testing strategies.",
"references": [
"https://developer.chrome.com/docs/extensions/",
"https://developer.chrome.com/docs/extensions/develop/migrate",
"https://developer.chrome.com/docs/extensions/develop/concepts/service-workers/lifecycle",
"https://developer.chrome.com/docs/extensions/develop/concepts/content-scripts",
"https://developer.chrome.com/docs/extensions/reference/api/storage",
"https://developer.chrome.com/docs/extensions/develop/concepts/messaging",
"https://developer.chrome.com/blog/longer-esw-lifetimes",
"https://developer.chrome.com/blog/Offscreen-Documents-in-Manifest-v3"
]
}
Chrome Extensions Best Practices
A comprehensive performance optimization guide for Chrome Extensions (Manifest V3), designed for AI agents and LLMs.
Overview
This skill contains 40+ rules across 8 categories, prioritized by impact from critical (service worker lifecycle, content script optimization) to incremental (API usage patterns).
Structure
chrome-extensions/
├── SKILL.md # Entry point with quick reference
├── AGENTS.md # Compiled comprehensive guide
├── metadata.json # Version, org, references
├── README.md # This file
├── references/
│ ├── _sections.md # Category definitions
│ ├── sw-*.md # Service worker rules
│ ├── content-*.md # Content script rules
│ ├── msg-*.md # Message passing rules
│ ├── storage-*.md # Storage operation rules
│ ├── net-*.md # Network & permissions rules
│ ├── mem-*.md # Memory management rules
│ ├── ui-*.md # UI performance rules
│ └── api-*.md # API usage pattern rules
└── assets/
└── templates/
└── _template.md # Rule templateGetting Started
Installation
# Clone the skills repository
git clone <repository-url>
cd chrome-extensions
# Install dependencies (if any validation scripts are used)
pnpm installBuild
# Build the AGENTS.md compiled document
pnpm buildValidate
# Validate skill structure and content
pnpm validateCreating a New Rule
1. Identify the appropriate category from references/_sections.md 2. Create a new file using the naming pattern: {prefix}-{description}.md 3. Use the template from assets/templates/_template.md 4. Add the rule to the quick reference in SKILL.md 5. Rebuild AGENTS.md
Prefix Reference
| Category | Prefix | Impact |
|---|---|---|
| Service Worker Lifecycle | sw- | CRITICAL |
| Content Script Optimization | content- | CRITICAL |
| Message Passing Efficiency | msg- | HIGH |
| Storage Operations | storage- | HIGH |
| Network & Permissions | net- | MEDIUM-HIGH |
| Memory Management | mem- | MEDIUM |
| UI Performance | ui- | MEDIUM |
| API Usage Patterns | api- | LOW-MEDIUM |
Rule File Structure
---
title: Rule Title Here
impact: CRITICAL|HIGH|MEDIUM-HIGH|MEDIUM|LOW-MEDIUM|LOW
impactDescription: Quantified impact (e.g., "2-10× improvement")
tags: prefix, keyword1, keyword2
---
## Rule Title Here
Brief explanation of WHY this matters (1-3 sentences).
**Incorrect (problem description):**
\`\`\`javascript
// Bad code example
\`\`\`
**Correct (solution description):**
\`\`\`javascript
// Good code example
\`\`\`
Reference: [Source Title](URL)File Naming Convention
Rule files use the pattern: {prefix}-{description}.md
- prefix: Category identifier (3-8 lowercase characters)
- description: Kebab-case description of the rule
Examples:
sw-persist-state-storage.mdcontent-use-specific-matches.mdmsg-debounce-frequent-events.md
Impact Levels
| Level | Description |
|---|---|
| CRITICAL | Fundamental issues causing major performance problems |
| HIGH | Significant impact, should be addressed in most cases |
| MEDIUM-HIGH | Notable impact, recommended to address |
| MEDIUM | Moderate impact, worth considering |
| LOW-MEDIUM | Minor impact, nice to have |
| LOW | Minimal impact, edge case optimizations |
Scripts
| Script | Description |
|---|---|
pnpm build | Compile all rules into AGENTS.md |
pnpm validate | Validate skill structure and content |
Contributing
1. Fork the repository 2. Create a feature branch 3. Add or modify rules following the template 4. Run validation 5. Submit a pull request
Guidelines
- Each rule must have incorrect AND correct code examples
- Impact must be quantified where possible
- First tag must be the category prefix
- Code examples should be production-realistic
- Use imperative mood in titles ("Use X" not "Using X")
Acknowledgments
Based on official documentation from:
Sections
This file defines all sections, their ordering, impact levels, and descriptions. The section ID (in parentheses) is the filename prefix used to group rules.
---
1. Service Worker Lifecycle (sw)
Impact: CRITICAL Description: Service workers are ephemeral with 30-second idle timeouts. Global state loss, improper persistence, and unnecessary keep-alive patterns are the #1 performance killers in Manifest V3 extensions.
2. Content Script Optimization (content)
Impact: CRITICAL Description: Content scripts run on every matching page. Heavy script injection, poor timing, and DOM manipulation bottlenecks cascade across all user browsing sessions.
3. Message Passing Efficiency (msg)
Impact: HIGH Description: Cross-context messaging between service workers, content scripts, and popups adds latency. Poor patterns cause N×M message storms and serialization overhead.
4. Storage Operations (storage)
Impact: HIGH Description: Storage I/O is asynchronous and can block extension responsiveness. Excessive reads/writes, wrong storage type selection, and unbatched operations create significant delays.
5. Network & Permissions (net)
Impact: MEDIUM-HIGH Description: Over-requesting permissions triggers store rejection. Using webRequest instead of declarativeNetRequest blocks the main thread and degrades browsing performance.
6. Memory Management (mem)
Impact: MEDIUM Description: Memory leaks from detached DOM nodes, uncleaned event listeners, and closure retention accumulate over the extension's lifetime, eventually degrading browser performance.
7. UI Performance (ui)
Impact: MEDIUM Description: Popup startup time, options page rendering, and badge updates affect perceived responsiveness and user experience.
8. API Usage Patterns (api)
Impact: LOW-MEDIUM Description: Chrome API misuse including wrong timer APIs, sync vs async patterns, and inefficient query patterns causes subtle but cumulative performance degradation.
Avoid Redundant API Calls in Loops
Calling Chrome APIs inside loops creates unnecessary overhead. Fetch data once before the loop, or batch operations where the API supports it.
Incorrect (API call per iteration):
// content.js - Reads storage for each element
async function processElements() {
const elements = document.querySelectorAll('.item');
for (const element of elements) {
// Storage read on EVERY element
const { settings } = await chrome.storage.local.get('settings');
applySettings(element, settings);
}
}
// 100 elements = 100 storage readsCorrect (single API call before loop):
// content.js - Read once, use many times
async function processElements() {
const { settings } = await chrome.storage.local.get('settings');
const elements = document.querySelectorAll('.item');
for (const element of elements) {
applySettings(element, settings);
}
}
// 100 elements = 1 storage readBatch tab operations:
// background.js - Incorrect: update tabs one by one
async function muteAllTabs() {
const tabs = await chrome.tabs.query({ audible: true });
for (const tab of tabs) {
await chrome.tabs.update(tab.id, { muted: true }); // Sequential
}
}
// background.js - Correct: parallel batch update
async function muteAllTabs() {
const tabs = await chrome.tabs.query({ audible: true });
await Promise.all(
tabs.map(tab => chrome.tabs.update(tab.id, { muted: true }))
);
}Batch storage writes:
// Incorrect: separate writes
for (const item of items) {
await chrome.storage.local.set({ [item.id]: item });
}
// Correct: single batched write
const updates = Object.fromEntries(items.map(i => [i.id, i]));
await chrome.storage.local.set(updates);Reference: chrome.storage API
Handle Extension Context Invalidated Errors
When an extension is updated or reloaded, existing content scripts become orphaned. Their chrome.* API calls will throw "Extension context invalidated" errors. Handle this gracefully.
Incorrect (crashes on extension update):
// content.js - Errors after extension update
setInterval(async () => {
// Throws error if extension was updated
const { settings } = await chrome.storage.local.get('settings');
applySettings(settings);
}, 5000);
document.addEventListener('click', async (event) => {
// Throws error if extension was reloaded
await chrome.runtime.sendMessage({ type: 'click', target: event.target.id });
});Correct (graceful degradation):
// content.js - Handle context invalidation
function isExtensionContextValid() {
try {
// Quick check if extension context is still valid
return !!chrome.runtime?.id;
} catch {
return false;
}
}
async function safeStorageGet(keys) {
if (!isExtensionContextValid()) {
console.warn('Extension context invalidated');
cleanup();
return null;
}
try {
return await chrome.storage.local.get(keys);
} catch (error) {
if (error.message?.includes('Extension context invalidated')) {
cleanup();
return null;
}
throw error;
}
}
async function safeSendMessage(message) {
if (!isExtensionContextValid()) {
return null;
}
try {
return await chrome.runtime.sendMessage(message);
} catch (error) {
if (error.message?.includes('Extension context invalidated')) {
cleanup();
return null;
}
throw error;
}
}
function cleanup() {
// Clear intervals, remove listeners, hide UI elements
clearAllIntervals();
removeInjectedElements();
console.log('Content script cleaned up after context invalidation');
}Detect extension reload:
// content.js - Listen for disconnect
const port = chrome.runtime.connect({ name: 'heartbeat' });
port.onDisconnect.addListener(() => {
if (chrome.runtime.lastError) {
// Extension was reloaded or disabled
cleanup();
}
});Reference: Content Script Lifecycle
Query Tabs with Specific Filters
Querying all tabs and filtering in JavaScript wastes resources. Use chrome.tabs.query filter options to let the browser return only relevant tabs.
Incorrect (query all, filter in JS):
// background.js - Fetches all tabs, filters manually
async function getGitHubTabs() {
const allTabs = await chrome.tabs.query({}); // All tabs
return allTabs.filter(tab =>
tab.url?.includes('github.com')
);
}
async function getActiveTabs() {
const allTabs = await chrome.tabs.query({});
return allTabs.filter(tab => tab.active); // Most are false
}Correct (let browser filter):
// background.js - Browser returns filtered results
async function getGitHubTabs() {
return chrome.tabs.query({
url: ['https://github.com/*', 'https://gist.github.com/*']
});
}
async function getActiveTab() {
const [tab] = await chrome.tabs.query({
active: true,
currentWindow: true
});
return tab;
}
async function getAudibleTabs() {
return chrome.tabs.query({ audible: true });
}Useful query filters:
// Pinned tabs
const pinned = await chrome.tabs.query({ pinned: true });
// Tabs with unsaved content
const unsaved = await chrome.tabs.query({ autoDiscardable: false });
// Tabs in specific window
const windowTabs = await chrome.tabs.query({ windowId: someWindowId });
// Muted tabs
const muted = await chrome.tabs.query({ muted: true });
// Discarded (sleeping) tabs
const discarded = await chrome.tabs.query({ discarded: true });Note: URL filtering requires tabs permission or appropriate host permissions.
Reference: chrome.tabs.query
Respect Alarms API Minimum Period
The chrome.alarms API enforces a minimum period of 1 minute. Shorter values are silently rounded up, which can cause unexpected behavior if you're expecting sub-minute intervals.
Incorrect (assumes short intervals work):
// background.js - Won't work as expected
chrome.runtime.onInstalled.addListener(() => {
// These all become 1-minute alarms
chrome.alarms.create('quick-poll', { periodInMinutes: 0.1 }); // Rounded to 1
chrome.alarms.create('check', { delayInMinutes: 0.5 }); // Rounded to 1
chrome.alarms.create('update', { periodInMinutes: 0.25 }); // Rounded to 1
});
// Expecting 10-second intervals, gets 60-second intervalsCorrect (design for minimum constraints):
// background.js - Use appropriate timing
chrome.runtime.onInstalled.addListener(() => {
// Respect the 1-minute minimum
chrome.alarms.create('sync-data', { periodInMinutes: 1 });
// For less frequent operations, use longer periods
chrome.alarms.create('daily-report', { periodInMinutes: 1440 });
});
// For sub-minute polling while SW is active, combine approaches
let pollingInterval = null;
function startActivePolling() {
// Use setInterval while actively processing
pollingInterval = setInterval(checkForUpdates, 5000);
// Backup alarm ensures recovery if SW dies
chrome.alarms.create('polling-backup', { periodInMinutes: 1 });
}
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'polling-backup') {
checkForUpdates();
startActivePolling(); // Resume fast polling
}
});When you need sub-minute updates:
- Keep service worker active with ongoing operations
- Use setInterval while actively processing
- Accept 1-minute minimum for background wake-ups
Reference: chrome.alarms API
Use Declarative Content API for Page Actions
Instead of waking the service worker on every navigation to check if your icon should be enabled, use declarativeContent to let the browser handle it natively.
Incorrect (SW wakes on every navigation):
// background.js - Wakes SW on every tab update
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete') {
// Check every page load
if (tab.url?.includes('github.com')) {
chrome.action.enable(tabId);
chrome.action.setIcon({ tabId, path: 'icon-active.png' });
} else {
chrome.action.disable(tabId);
}
}
});Correct (browser handles declaratively):
// background.js - Runs once at install, browser handles rest
chrome.runtime.onInstalled.addListener(() => {
// Clear any existing rules
chrome.declarativeContent.onPageChanged.removeRules(undefined, () => {
// Add new rules
chrome.declarativeContent.onPageChanged.addRules([{
conditions: [
new chrome.declarativeContent.PageStateMatcher({
pageUrl: { hostSuffix: 'github.com' }
}),
new chrome.declarativeContent.PageStateMatcher({
pageUrl: { hostSuffix: 'gitlab.com' }
})
],
actions: [
new chrome.declarativeContent.ShowAction()
]
}]);
});
});Manifest configuration:
{
"action": {
"default_icon": "icon.png"
},
"permissions": ["declarativeContent"]
}Available conditions:
pageUrl- Match URL patternscss- Match pages with specific CSS selectorsisBookmarked- Page is bookmarked
Available actions:
ShowAction- Enable the action iconSetIcon- Change the iconRequestContentScript- Inject content script
Reference: chrome.declarativeContent
Use Promise-Based API Calls Over Callbacks
Modern Chrome Extension APIs support promises. Promise-based calls are cleaner, support async/await, and make error handling easier with try/catch.
Incorrect (callback hell):
// background.js - Nested callbacks, hard to read
chrome.tabs.query({ active: true }, (tabs) => {
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError);
return;
}
chrome.tabs.sendMessage(tabs[0].id, { type: 'get-data' }, (response) => {
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError);
return;
}
chrome.storage.local.set({ data: response }, () => {
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError);
return;
}
console.log('Data saved');
});
});
});Correct (async/await with promises):
// background.js - Clean, linear flow
async function processActiveTab() {
try {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
const response = await chrome.tabs.sendMessage(tab.id, { type: 'get-data' });
await chrome.storage.local.set({ data: response });
console.log('Data saved');
} catch (error) {
console.error('Operation failed:', error);
}
}Promise.all for parallel operations:
// background.js - Parallel API calls
async function gatherExtensionData() {
const [tabs, storage, bookmarks] = await Promise.all([
chrome.tabs.query({}),
chrome.storage.local.get(null),
chrome.bookmarks.getTree()
]);
return { tabs, storage, bookmarks };
}Note: All Chrome Extension APIs in the chrome.* namespace support promises as of Manifest V3. The browser.* namespace (available since Chrome 144) also uses promises.
Reference: Chrome Extension APIs
Extension adapter interface pattern
Define adapter interfaces for communication between extension contexts. This decouples UI components from background implementation.
Incorrect
// Direct chrome API calls in UI components
function PopupBody(): JSX.Element {
const handleClick = async () => {
const tabs = await chrome.tabs.query({ active: true });
await chrome.storage.local.set({ enabled: true });
chrome.runtime.sendMessage({ type: 'toggle' });
};
}
// Untyped message passing
function sendMessage(msg: any): void {
chrome.runtime.sendMessage(msg);
}Correct
// Define the adapter interface
export interface ExtensionAdapter {
// Data retrieval
collect(): Promise<ExtensionData>;
getActiveTabInfo(): Promise<TabInfo>;
// Settings
changeSettings(settings: Partial<UserSettings>): void;
setTheme(theme: Partial<Theme>): void;
// Actions
toggleActiveTab(): void;
markNewsAsRead(ids: string[]): void;
}
// Implement adapter in background context
function createBackgroundAdapter(): ExtensionAdapter {
return {
collect: () => Extension.collect(),
getActiveTabInfo: () => TabManager.getActiveTabInfo(),
changeSettings: (settings) => UserStorage.changeSettings(settings),
setTheme: (theme) => UserStorage.setTheme(theme),
toggleActiveTab: () => Extension.toggleActiveTab(),
markNewsAsRead: (ids) => NewsManager.markAsRead(ids),
};
}
// Use adapter in UI components
interface PopupProps {
adapter: ExtensionAdapter;
}
function PopupBody({ adapter }: PopupProps): JSX.Element {
const handleToggle = () => {
adapter.toggleActiveTab();
};
return <Button onClick={handleToggle}>Toggle</Button>;
}Messenger Pattern for Remote Adapter
// In popup (UI context)
function createUIAdapter(): ExtensionAdapter {
return {
collect: () => Messenger.sendAndWait(MessageTypeUItoBG.GET_DATA),
changeSettings: (settings) => Messenger.send({
type: MessageTypeUItoBG.CHANGE_SETTINGS,
data: settings,
}),
toggleActiveTab: () => Messenger.send({
type: MessageTypeUItoBG.TOGGLE_ACTIVE_TAB,
}),
};
}Benefits
- Testability: UI components can use mock adapters in tests
- Type safety: All operations are typed
- Decoupling: UI doesn't know about chrome APIs or message passing
- Flexibility: Can swap implementation (e.g., for different browsers)
Content script structure
Organize content scripts with clear initialization, message handling, and cleanup phases. Always handle the extension context invalidation.
Incorrect
// Immediate execution without structure
const theme = document.createElement('style');
document.head.appendChild(theme);
chrome.runtime.onMessage.addListener((msg) => {
if (msg.type === 'update') {
theme.textContent = msg.css;
}
});Correct
// src/inject/index.ts
import { createOrUpdateStyle, removeStyle } from './style-manager';
import type { Message, Theme } from '../definitions';
let isExtensionActive = true;
function onMessage(message: Message): void {
if (!isExtensionActive) return;
switch (message.type) {
case MessageTypeBGtoCS.ADD_CSS_FILTER:
createOrUpdateStyle(message.data);
break;
case MessageTypeBGtoCS.CLEAN_UP:
cleanup();
break;
}
}
function cleanup(): void {
removeStyle();
isExtensionActive = false;
}
function init(): void {
// Check if already injected (multiple injection protection)
if (document.documentElement.dataset.darkreaderInjected) {
return;
}
document.documentElement.dataset.darkreaderInjected = 'true';
try {
chrome.runtime.onMessage.addListener(onMessage);
// Notify background that content script is ready
chrome.runtime.sendMessage({
type: MessageTypeCStoBG.DOCUMENT_CONNECT,
data: { url: location.href },
});
} catch (e) {
// Extension context invalidated
cleanup();
}
}
// Wait for DOM to be ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}Style Manager Module
// src/inject/style-manager.ts
const STYLE_ID = 'dark-reader-style';
let styleElement: HTMLStyleElement | null = null;
export function createOrUpdateStyle(css: string): void {
if (!styleElement) {
styleElement = document.createElement('style');
styleElement.id = STYLE_ID;
styleElement.type = 'text/css';
(document.head || document.documentElement).appendChild(styleElement);
}
styleElement.textContent = css;
}
export function removeStyle(): void {
if (styleElement) {
styleElement.remove();
styleElement = null;
}
}Key Principles
1. Multiple injection protection: Check if already injected via data attribute 2. Cleanup function: Always have a way to fully remove injected content 3. Context check: Guard message handlers against stale extension context 4. DOM readiness: Wait for DOM before manipulating it
CSS class patterns
Use BEM-inspired class naming with double underscores for elements and double dashes for modifiers.
BEM Convention
| Type | Separator | Example |
|---|---|---|
| Block | - | slider, settings-list |
| Element | __ | slider__track, slider__thumb |
| Modifier | -- | slider--active, button--primary |
Incorrect
// CamelCase classes
<div class="sliderTrack sliderTrackActive">
// Inconsistent separators
<div class="slider_track slider-active">
// String concatenation for conditionals
<button class={`button ${isActive ? 'button-active' : ''}`}>Correct
// BEM naming
<div class="slider">
<div class="slider__track">
<div class="slider__fill" />
<div class="slider__thumb" />
</div>
</div>
// Conditional classes with object syntax
<button class={{
'button': true,
'button--primary': isPrimary,
'button--disabled': isDisabled,
}}>
// Element with modifier
<div class={{
'slider__track': true,
'slider__track--active': isActive,
}}>CSS Structure
/* Block */
.slider {
position: relative;
width: 100%;
}
/* Element */
.slider__track {
height: 4px;
background: var(--color-track);
}
.slider__thumb {
width: 16px;
height: 16px;
border-radius: 50%;
}
/* Modifier */
.slider--disabled {
opacity: 0.5;
pointer-events: none;
}
.slider__thumb--active {
transform: scale(1.2);
}Class Merge Utility
// Utility to merge base class with additional classes
function mergeClass(base: string, additional?: string): string {
return additional ? `${base} ${additional}` : base;
}
// Usage
function Button(props: ButtonProps): JSX.Element {
const cls = mergeClass('button', props.class);
return <button class={cls}>{props.children}</button>;
}Why This Matters
- Predictability: Class names follow a pattern you can rely on
- Specificity: BEM keeps specificity flat (single class selectors)
- Scoping: Element classes are visually scoped to their block
- Searchability: Easy to find all related styles
Manager class pattern for background
Use classes with static methods for singleton managers in background scripts. Initialize with a static init() method.
Incorrect
// Object literal lacks type safety and clear initialization
const messenger = {
adapter: null,
init: (adapter) => { messenger.adapter = adapter; },
send: (msg) => { messenger.adapter.send(msg); },
};
// Instantiated class creates multiple instances
class TabManager {
constructor(private adapter: ExtensionAdapter) {}
}
const manager = new TabManager(adapter);Correct
export default class Messenger {
private static adapter: ExtensionAdapter;
private static onMessage: (msg: Message) => void;
static init(adapter: ExtensionAdapter): void {
Messenger.adapter = adapter;
chrome.runtime.onMessage.addListener(Messenger.handleMessage);
}
private static handleMessage(
message: Message,
sender: chrome.runtime.MessageSender,
sendResponse: (response: any) => void
): boolean {
// Handle message
return true; // Keep channel open for async response
}
static sendToContentScript(tabId: number, message: Message): void {
chrome.tabs.sendMessage(tabId, message);
}
}
// In background entry point
import Messenger from './messenger';
import TabManager from './tab-manager';
import UserStorage from './user-storage';
async function init(): Promise<void> {
await UserStorage.init();
TabManager.init();
Messenger.init(createAdapter());
}Benefits
- Single instance: Static methods guarantee one instance per context
- Clear initialization:
init()method explicitly sets up the manager - Testable: Can mock static methods in tests
- Type safety: Full TypeScript support for all methods
- Encapsulation: Private static members hide implementation details
When NOT to Use
For simple utilities that don't need state, use plain functions instead:
// Good: stateless utility
export function parseURL(url: string): URL | null { }
// Overkill: no state needed
export class URLParser {
static parse(url: string): URL | null { }
}Type guard functions
Create type guard functions for validating unknown data at runtime. Name them with is prefix and use x is T return type.
Basic Type Guards
// Primitive type guards
function isBoolean(x: unknown): x is boolean {
return typeof x === 'boolean';
}
function isString(x: unknown): x is string {
return typeof x === 'string';
}
function isNumber(x: unknown): x is number {
return typeof x === 'number' && !isNaN(x);
}
function isNonEmptyString(x: unknown): x is string {
return typeof x === 'string' && x.length > 0;
}
// Usage
function processValue(value: unknown): void {
if (isString(value)) {
// TypeScript knows value is string here
console.log(value.toLowerCase());
}
}Complex Object Type Guards
interface UserSettings {
enabled: boolean;
brightness: number;
theme: Theme;
}
function isUserSettings(x: unknown): x is UserSettings {
if (typeof x !== 'object' || x === null) {
return false;
}
const obj = x as Record<string, unknown>;
return (
isBoolean(obj.enabled) &&
isNumber(obj.brightness) &&
isTheme(obj.theme)
);
}
// Partial object guard
function isPartialUserSettings(x: unknown): x is Partial<UserSettings> {
if (typeof x !== 'object' || x === null) {
return false;
}
const obj = x as Record<string, unknown>;
if ('enabled' in obj && !isBoolean(obj.enabled)) return false;
if ('brightness' in obj && !isNumber(obj.brightness)) return false;
if ('theme' in obj && !isTheme(obj.theme)) return false;
return true;
}Message Type Guards
interface GetDataMessage {
type: MessageTypeUItoBG.GET_DATA;
}
interface ChangeSettingsMessage {
type: MessageTypeUItoBG.CHANGE_SETTINGS;
data: Partial<UserSettings>;
}
type UIMessage = GetDataMessage | ChangeSettingsMessage;
function isGetDataMessage(msg: unknown): msg is GetDataMessage {
return (
typeof msg === 'object' &&
msg !== null &&
(msg as GetDataMessage).type === MessageTypeUItoBG.GET_DATA
);
}
function isChangeSettingsMessage(msg: unknown): msg is ChangeSettingsMessage {
return (
typeof msg === 'object' &&
msg !== null &&
(msg as ChangeSettingsMessage).type === MessageTypeUItoBG.CHANGE_SETTINGS &&
isPartialUserSettings((msg as ChangeSettingsMessage).data)
);
}
// Usage in message handler
function handleMessage(message: unknown): void {
if (isGetDataMessage(message)) {
return handleGetData();
}
if (isChangeSettingsMessage(message)) {
// TypeScript knows message.data is Partial<UserSettings>
return handleChangeSettings(message.data);
}
logWarn('Unknown message type:', message);
}Array Type Guards
function isArrayOf<T>(
arr: unknown,
guard: (item: unknown) => item is T
): arr is T[] {
return Array.isArray(arr) && arr.every(guard);
}
// Usage
function isStringArray(x: unknown): x is string[] {
return isArrayOf(x, isString);
}
function isTabInfoArray(x: unknown): x is TabInfo[] {
return isArrayOf(x, isTabInfo);
}Why This Matters
- Type safety: Validate external data matches expected types
- Type narrowing: TypeScript understands the type after the check
- Reusability: Type guards can be composed and reused
- Runtime safety: Catches type mismatches that TypeScript can't see
UI component patterns
Structure UI components with explicit props interface, typed state, and consistent export patterns.
Component File Structure
// src/ui/controls/slider/index.tsx
import { useState, useRef } from 'preact/hooks';
interface SliderProps {
value: number;
min: number;
max: number;
step?: number;
class?: string;
onChange: (value: number) => void;
}
interface SliderState {
isActive: boolean;
displayValue: number;
}
export default function Slider(props: SliderProps): JSX.Element {
const { value, min, max, step = 1, onChange } = props;
const [state, setState] = useState<SliderState>({
isActive: false,
displayValue: value,
});
const trackRef = useRef<HTMLDivElement>(null);
const handlePointerDown = (e: PointerEvent): void => {
setState((prev) => ({ ...prev, isActive: true }));
// Handle interaction
};
const percentage = ((value - min) / (max - min)) * 100;
return (
<div
class={`slider ${props.class || ''}`}
ref={trackRef}
onPointerDown={handlePointerDown}
>
<div class="slider__track">
<div
class="slider__fill"
style={{ width: `${percentage}%` }}
/>
<div
class="slider__thumb"
style={{ left: `${percentage}%` }}
/>
</div>
</div>
);
}Component Organization
src/ui/
├── controls/ # Reusable UI controls
│ ├── button/
│ │ └── index.tsx
│ ├── slider/
│ │ └── index.tsx
│ └── checkbox/
│ └── index.tsx
├── popup/
│ ├── index.tsx # Popup entry point
│ ├── body/
│ │ └── index.tsx
│ └── components/
│ ├── header/
│ └── settings-list/
└── options/
└── index.tsxProps Interface Pattern
// Common attribute props pattern
interface ButtonProps {
class?: string;
disabled?: boolean;
onClick?: () => void;
}
// With children
interface ContainerProps {
class?: string;
children: ComponentChildren;
}
// Required vs optional
interface SettingsProps {
settings: UserSettings; // Required
theme?: Theme; // Optional
onSettingsChange: (s: Partial<UserSettings>) => void;
}State Interface Pattern
interface BodyState {
activeTab: string;
isNewsOpen: boolean;
didNewsSlideIn: boolean;
}
// Initialize with all properties
const [state, setState] = useState<BodyState>({
activeTab: 'filter',
isNewsOpen: false,
didNewsSlideIn: false,
});
// Update specific properties
setState((prev) => ({
...prev,
isNewsOpen: true,
}));Batch DOM Operations to Minimize Reflows
Reading layout properties and modifying the DOM in alternating sequence causes layout thrashing. Batch all reads together, then all writes, to minimize expensive browser reflow calculations.
Incorrect (layout thrashing with read-write alternation):
// content.js - Forces reflow on every iteration
function highlightElements(selectors) {
selectors.forEach(selector => {
const element = document.querySelector(selector);
const height = element.offsetHeight; // Read - forces layout
element.style.height = height + 10 + 'px'; // Write - invalidates
const width = element.offsetWidth; // Read - forces reflow again
element.style.width = width + 10 + 'px'; // Write - invalidates again
});
}
// N elements = 4N layout calculationsCorrect (batched reads then writes):
// content.js - Single reflow for all operations
function highlightElements(selectors) {
const elements = selectors.map(s => document.querySelector(s));
// Batch all reads first
const measurements = elements.map(el => ({
element: el,
height: el.offsetHeight,
width: el.offsetWidth
}));
// Batch all writes together
measurements.forEach(({ element, height, width }) => {
element.style.height = height + 10 + 'px';
element.style.width = width + 10 + 'px';
});
}
// N elements = 2 layout calculations totalAlternative (using DocumentFragment):
// content.js - Build DOM off-screen
function createOverlay(items) {
const fragment = document.createDocumentFragment();
items.forEach(item => {
const div = document.createElement('div');
div.className = 'overlay-item';
div.textContent = item.label;
fragment.appendChild(div); // No reflow yet
});
document.body.appendChild(fragment); // Single reflow
}Properties that trigger layout: offsetHeight, offsetWidth, offsetTop, clientHeight, scrollHeight, getComputedStyle(), getBoundingClientRect()
Reference: What forces layout/reflow
Minimize Content Script Bundle Size
Content script JavaScript must be parsed and compiled on every page load. Unlike web pages, extension scripts don't benefit from HTTP cache for compilation. Large bundles significantly slow down every page the user visits.
Incorrect (large monolithic bundle):
// content.js - 200KB bundle with unused code
import React from 'react'; // 40KB
import ReactDOM from 'react-dom'; // 40KB
import lodash from 'lodash'; // 70KB
import moment from 'moment'; // 50KB
// Only uses 2 functions from each library
const result = lodash.debounce(() => {});
const date = moment().format('YYYY-MM-DD');Correct (minimal targeted imports):
// content.js - 5KB bundle with only what's needed
import debounce from 'lodash/debounce'; // 1KB
// Use native alternatives when possible
const date = new Date().toISOString().split('T')[0];
// Lazy-load heavy features
let heavyModule = null;
async function loadHeavyFeature() {
if (!heavyModule) {
heavyModule = await import('./heavy-feature.js');
}
return heavyModule;
}Bundle optimization strategies:
- Use tree-shakeable ES modules
- Import specific functions, not entire libraries
- Use native APIs instead of libraries (Date vs moment)
- Split code and lazy-load non-critical features
- Analyze bundle with tools like webpack-bundle-analyzer
Target sizes:
- Simple content scripts: < 10KB
- Feature-rich scripts: < 50KB
- Heavy UI overlays: lazy-load separately
Reference: Content Scripts
Prefer Programmatic Injection Over Manifest Declaration
Manifest-declared content scripts run on every matching page load, even if unused. Programmatic injection using chrome.scripting.executeScript loads scripts only when the user actually needs the feature.
Incorrect (always loaded on every matching page):
{
"content_scripts": [{
"matches": ["https://*.com/*"],
"js": ["page-analyzer.js"]
}]
}// page-analyzer.js - 50KB of code loaded on every page
// Even if user never clicks the extension
import { analyzeDOM } from './analyzer';
import { renderOverlay } from './overlay';
// ...all this code parsed and compiled on every page visitCorrect (loaded only when user clicks):
{
"permissions": ["activeTab", "scripting"]
}// background.js
chrome.action.onClicked.addListener(async (tab) => {
await chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ['page-analyzer.js']
});
});
// popup.js (or from popup button click)
document.getElementById('analyze-btn').addEventListener('click', async () => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
await chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ['page-analyzer.js']
});
});Benefits of programmatic injection:
activeTabpermission shows no warning (vs host permissions)- Zero performance impact on pages where feature isn't used
- Script can be injected into any tab when needed
- Smaller initial extension footprint
When manifest declaration IS appropriate:
- Script must observe page from very beginning
- Script modifies page appearance immediately (CSS injection)
- Script intercepts page events before they fire
Reference: chrome.scripting API
Use document_idle for Content Script Injection
The default document_idle timing injects scripts after the DOM is ready but before all resources load. Using document_start blocks the page while your script runs. Only use earlier injection when absolutely necessary.
Incorrect (blocks page rendering):
{
"content_scripts": [{
"matches": ["https://example.com/*"],
"js": ["content.js"],
"run_at": "document_start"
}]
}// content.js - Heavy initialization at document_start
const config = loadExtensionConfig(); // Synchronous
setupMutationObservers();
initializeFeatures();
// All this runs before page renders anythingCorrect (non-blocking injection):
{
"content_scripts": [{
"matches": ["https://example.com/*"],
"js": ["content.js"],
"run_at": "document_idle"
}]
}// content.js - Runs after DOM is ready
async function initialize() {
const config = await chrome.storage.local.get(['config']);
setupMutationObservers();
initializeFeatures();
}
initialize();When document_start IS appropriate:
- Injecting CSS to prevent flash of unstyled content
- Intercepting/modifying page scripts before they run
- Observing very early DOM mutations
Injection timing reference:
document_start- Before DOM construction beginsdocument_end- After DOM ready, before subresourcesdocument_idle- After DOM ready, during/after subresource load (default)
Reference: Content Scripts
Use MutationObserver Instead of Polling for DOM Changes
Polling the DOM with setInterval wastes CPU cycles checking for changes that haven't happened. MutationObserver is event-driven and only runs when the DOM actually changes.
Incorrect (continuous polling wastes CPU):
// content.js - Checks every 100ms even when nothing changes
let lastContent = null;
setInterval(() => {
const element = document.querySelector('.dynamic-content');
if (element && element.textContent !== lastContent) {
lastContent = element.textContent;
processNewContent(element);
}
}, 100); // 10 checks per second, 600 per minuteCorrect (event-driven observation):
// content.js - Only runs when DOM actually changes
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
const newContent = mutation.target.querySelector('.dynamic-content');
if (newContent) {
processNewContent(newContent);
}
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// Clean up when content script is done
window.addEventListener('unload', () => observer.disconnect());Optimized observation (narrow scope):
// content.js - Watch only the relevant container
async function watchForContent() {
// Wait for container to exist
const container = await waitForElement('#app-container');
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
processAddedNodes(mutation.addedNodes);
}
});
observer.observe(container, {
childList: true,
subtree: false // Don't watch deeply if not needed
});
}
function waitForElement(selector) {
return new Promise(resolve => {
const el = document.querySelector(selector);
if (el) return resolve(el);
const observer = new MutationObserver(() => {
const el = document.querySelector(selector);
if (el) {
observer.disconnect();
resolve(el);
}
});
observer.observe(document.body, { childList: true, subtree: true });
});
}Reference: MutationObserver MDN
Use Specific URL Match Patterns Instead of All URLs
Using <all_urls> or overly broad match patterns injects your content script into every page, slowing down all browsing and increasing memory usage. Specify the exact domains and paths your extension needs.
Incorrect (injected into every page):
{
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["content.js"]
}]
}{
"content_scripts": [{
"matches": ["*://*/*"],
"js": ["content.js"]
}]
}Correct (injected only where needed):
{
"content_scripts": [{
"matches": [
"https://github.com/*",
"https://gitlab.com/*"
],
"js": ["content.js"]
}]
}Even better (path-specific):
{
"content_scripts": [{
"matches": [
"https://github.com/*/*/pull/*",
"https://github.com/*/*/issues/*"
],
"js": ["pr-tools.js"]
}]
}If you need broad access conditionally:
// background.js - Inject only when needed
chrome.action.onClicked.addListener(async (tab) => {
// User explicitly requested the feature
await chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ['content.js']
});
});Note: Over-requesting permissions is the #1 rejection reason in Chrome Web Store review.
Reference: Match Patterns
Extension context invalidation handling
Handle "Extension context invalidated" errors gracefully in content scripts. This error occurs when the extension updates or reloads while a content script is still running on a page.
The Problem
When a browser extension updates: 1. The background script restarts immediately 2. Content scripts already injected in tabs continue running 3. Those content scripts can no longer communicate with the extension 4. Any chrome.runtime call throws "Extension context invalidated"
Incorrect
// Unprotected messaging - will throw after extension update
function notifyBackground(data: unknown): void {
chrome.runtime.sendMessage({ type: 'update', data });
}
// Event listener keeps running after context invalidated
chrome.runtime.onMessage.addListener((message) => {
handleMessage(message); // Will fail
});Correct
let isContextValid = true;
function checkContext(): boolean {
try {
// Simple check - will throw if context invalid
void chrome.runtime.id;
return true;
} catch {
return false;
}
}
function sendMessageSafe(message: Message): void {
if (!isContextValid) return;
try {
chrome.runtime.sendMessage(message);
} catch (error) {
if (error.message === 'Extension context invalidated.') {
handleContextInvalidation();
} else {
throw error;
}
}
}
function handleContextInvalidation(): void {
isContextValid = false;
// Clean up injected content
removeInjectedStyles();
removeInjectedElements();
// Remove event listeners
document.removeEventListener('visibilitychange', onVisibilityChange);
// Clear any intervals/timeouts
clearAllTimers();
// Optionally notify user
console.info('Dark Reader: Extension was updated. Please refresh the page.');
}
// Wrap message listener
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (!isContextValid) {
return false;
}
try {
return handleMessage(message, sender, sendResponse);
} catch (error) {
if (error.message === 'Extension context invalidated.') {
handleContextInvalidation();
return false;
}
throw error;
}
});Port-based Communication Pattern
// Using ports for more reliable communication
let port: chrome.runtime.Port | null = null;
function connectToBackground(): void {
try {
port = chrome.runtime.connect({ name: 'content-script' });
port.onMessage.addListener(onMessage);
port.onDisconnect.addListener(() => {
if (chrome.runtime.lastError) {
handleContextInvalidation();
} else {
// Normal disconnect - try to reconnect
setTimeout(connectToBackground, 1000);
}
});
} catch (error) {
handleContextInvalidation();
}
}
function sendMessage(message: Message): void {
if (!port || !isContextValid) return;
try {
port.postMessage(message);
} catch {
handleContextInvalidation();
}
}Why This Matters
- User experience: Clean degradation instead of broken functionality
- Resource cleanup: Prevent memory leaks from orphaned listeners
- Error prevention: Avoid console spam from failed API calls
- Extension updates: Users don't need to manually refresh all tabs
Early return for guard clauses
Use early returns for guard clauses to avoid deep nesting and make the "happy path" clear.
Incorrect
// Deep nesting makes main logic hard to find
function processTab(tab: chrome.tabs.Tab): void {
if (tab) {
if (tab.url) {
if (!tab.url.startsWith('chrome://')) {
if (tab.id !== undefined) {
const parsed = parseURL(tab.url);
if (parsed) {
// Finally, the actual logic...
applyTheme(tab.id, parsed.hostname);
}
}
}
}
}
}Correct
// Guard clauses at the top, main logic at normal indentation
function processTab(tab: chrome.tabs.Tab): void {
if (!tab?.url) {
return;
}
if (tab.url.startsWith('chrome://')) {
return;
}
if (tab.id === undefined) {
return;
}
const parsed = parseURL(tab.url);
if (!parsed) {
return;
}
// Main logic is clear and at base indentation
applyTheme(tab.id, parsed.hostname);
}Guard Clause Patterns
// Boolean check
function setEnabled(enabled: boolean): void {
if (!enabled) {
return;
}
// ... enable logic
}
// Type narrowing guard
function processMessage(message: unknown): void {
if (!isValidMessage(message)) {
return;
}
// message is now typed as Message
handleMessage(message);
}
// State guard
async function applyTheme(): Promise<void> {
if (!Extension.isEnabled) {
return;
}
if (Extension.startBarrier?.isPending()) {
await Extension.startBarrier.entry();
}
// Apply theme...
}
// Multiple conditions combined
function shouldProcessURL(url: string | undefined): boolean {
if (!url) return false;
const parsed = parseURL(url);
if (!parsed) return false;
if (SKIP_PROTOCOLS.includes(parsed.protocol)) return false;
return true;
}Async Guard Clauses
async function loadTabData(tabId: number): Promise<TabData | null> {
// Guards with early return
const tab = await chrome.tabs.get(tabId).catch(() => null);
if (!tab) {
return null;
}
if (!tab.url || tab.url.startsWith('chrome://')) {
return null;
}
// Main logic
const host = parseURL(tab.url)?.hostname;
const settings = await loadHostSettings(host);
return { tab, host, settings };
}Why This Matters
- Readability: Main logic isn't buried in nested conditions
- Maintainability: Easy to add new guards without restructuring
- Debugging: Clear which condition caused early exit
- Cognitive load: Don't need to track multiple nested conditions
Null coalescing and optional chaining
Use nullish coalescing (??) and optional chaining (?.) for accessing potentially undefined values. This is safer than || which treats 0, '', and false as falsy.
Incorrect
// || treats 0, '', false as falsy
const brightness = settings.brightness || 100; // 0 becomes 100!
const name = user.name || 'Anonymous'; // '' becomes 'Anonymous'
// Verbose null checks
let host: string;
if (tab && tab.url) {
const parsed = parseURL(tab.url);
if (parsed && parsed.host) {
host = parsed.host;
}
}
// Nested property access without checks
const color = theme.colors.primary.dark; // Throws if any is undefinedCorrect
// ?? only triggers on null/undefined
const brightness = settings.brightness ?? 100; // 0 stays 0
const name = user.name ?? 'Anonymous'; // '' stays ''
// Optional chaining for nested access
const host = tab?.url && parseURL(tab.url)?.host;
// Combined patterns
const color = theme?.colors?.primary?.dark ?? '#000000';
// With function calls
const shortcut = commands.find((cmd) => cmd.name === command)?.shortcut ?? null;
// Nullish assignment
settings.brightness ??= 100; // Only assign if null/undefinedCommon Patterns
// Safe array access
const firstItem = items?.[0];
const lastItem = items?.[items.length - 1];
// Safe method calls
element?.classList?.add('active');
callback?.();
// With Map/Set
const cached = cache.get(key) ?? computeValue(key);
// In destructuring with defaults
const { brightness = 100, contrast = 100 } = theme ?? {};
// Combining with type narrowing
function getTabHost(tab: chrome.tabs.Tab | undefined): string {
const url = tab?.url;
if (!url) return '';
return parseURL(url)?.hostname ?? '';
}When to Use || vs ??
// Use ?? for numbers (0 is valid)
const opacity = settings.opacity ?? 1;
// Use ?? for strings ('' might be valid)
const customCSS = settings.customCSS ?? '';
// Use || for truly empty checks where 0/'' should trigger default
const displayName = user.name || 'Unknown'; // '' treated as "no name"
// Use || for boolean-like expressions
const isEnabled = settings.enabled || settings.forceEnable;Why This Matters
- Bug prevention:
0,'',falseare valid values, not always "missing" - Cleaner code: Replaces verbose if-chains with single expressions
- Type safety: Works well with TypeScript's strict null checks
- Predictable: Clear distinction between "empty" and "missing"
PromiseBarrier for async coordination
Use the PromiseBarrier pattern for coordinating async operations that need to wait for initialization to complete. This prevents race conditions during extension startup.
The Problem
Browser extensions have complex startup sequences: 1. Background script starts 2. Storage needs to load 3. UI popup opens (may be before storage is ready) 4. Content scripts connect (may be before background is ready)
Without coordination, components may try to use uninitialized state.
Incorrect
// Polling with arbitrary timeout
let isReady = false;
async function waitForReady(): Promise<void> {
while (!isReady) {
await new Promise((r) => setTimeout(r, 100));
}
}
// Race condition: multiple callers may proceed before ready
async function getData(): Promise<Data> {
if (!isReady) {
await waitForReady();
}
return data; // May still not be ready!
}Correct
// PromiseBarrier implementation
class PromiseBarrier {
private promise: Promise<void>;
private resolve!: () => void;
private pending = true;
constructor() {
this.promise = new Promise<void>((resolve) => {
this.resolve = resolve;
});
}
isPending(): boolean {
return this.pending;
}
async entry(): Promise<void> {
return this.promise;
}
resolve(): void {
this.pending = false;
this.resolve();
}
}
// Usage in Extension class
class Extension {
static startBarrier: PromiseBarrier | null = new PromiseBarrier();
private static data: ExtensionData;
static async init(): Promise<void> {
// Load all required data
const settings = await UserStorage.loadSettings();
const theme = await UserStorage.loadTheme();
Extension.data = { settings, theme };
// Signal that initialization is complete
Extension.startBarrier!.resolve();
Extension.startBarrier = null; // Allow GC
}
static async collect(): Promise<ExtensionData> {
// Wait for initialization if still pending
if (Extension.startBarrier?.isPending()) {
await Extension.startBarrier.entry();
}
return Extension.data;
}
}
// In message handler
async function onGetData(): Promise<ExtensionData> {
// Safe - will wait for init if needed
return Extension.collect();
}Multiple Barriers Pattern
class ExtensionLifecycle {
static storageBarrier = new PromiseBarrier();
static uiBarrier = new PromiseBarrier();
static contentScriptBarrier = new PromiseBarrier();
static async initStorage(): Promise<void> {
await loadAllStorage();
this.storageBarrier.resolve();
}
static async initUI(): Promise<void> {
// UI depends on storage
await this.storageBarrier.entry();
await setupUI();
this.uiBarrier.resolve();
}
static async waitForFullInit(): Promise<void> {
await Promise.all([
this.storageBarrier.entry(),
this.uiBarrier.entry(),
]);
}
}Why This Matters
- No race conditions: Multiple callers safely wait for same initialization
- Clear dependencies: Barriers make initialization order explicit
- No polling: Efficient promise-based waiting
- One-time resolution: Barrier can only resolve once
Storage operation error handling
Wrap storage operations in try-catch blocks. Storage can be unavailable in private browsing, when quota is exceeded, or when the user has disabled storage.
Incorrect
// Unprotected storage access
sessionStorage.setItem('theme', JSON.stringify(theme));
const settings = JSON.parse(localStorage.getItem('settings') || '{}');
// Unprotected chrome.storage
const result = await chrome.storage.local.get('settings');Correct
// Session/local storage with try-catch
function saveToSession(key: string, value: unknown): void {
try {
sessionStorage.setItem(key, JSON.stringify(value));
} catch (err) {
// Storage unavailable (private browsing, quota exceeded)
logWarn('Session storage unavailable:', err);
}
}
function loadFromSession<T>(key: string): T | null {
try {
const raw = sessionStorage.getItem(key);
return raw ? JSON.parse(raw) : null;
} catch (err) {
return null;
}
}
// Chrome storage with error check
async function saveSettings(settings: UserSettings): Promise<void> {
await chrome.storage.local.set({ settings });
if (chrome.runtime.lastError) {
logWarn('Failed to save settings:', chrome.runtime.lastError);
}
}
// Chrome storage sync with fallback
async function loadSettings(): Promise<UserSettings> {
try {
const result = await chrome.storage.sync.get('settings');
if (chrome.runtime.lastError) {
throw new Error(chrome.runtime.lastError.message);
}
return result.settings ?? DEFAULT_SETTINGS;
} catch (err) {
// Fall back to local storage if sync fails
const local = await chrome.storage.local.get('settings');
return local.settings ?? DEFAULT_SETTINGS;
}
}Storage Wrapper Pattern
class StorageWrapper {
static async get<T>(key: string, defaultValue: T): Promise<T> {
try {
const result = await chrome.storage.local.get(key);
return result[key] ?? defaultValue;
} catch (err) {
logWarn(`Storage get failed for "${key}":`, err);
return defaultValue;
}
}
static async set(key: string, value: unknown): Promise<boolean> {
try {
await chrome.storage.local.set({ [key]: value });
return !chrome.runtime.lastError;
} catch (err) {
logWarn(`Storage set failed for "${key}":`, err);
return false;
}
}
}Why This Matters
- Private browsing: Storage APIs throw in incognito mode in some browsers
- Quota limits: Storage can fill up, especially for themes/large data
- User settings: Users can disable storage in browser settings
- Graceful degradation: Extension should work even if storage fails
URL parsing with fallback
Always wrap URL parsing in try-catch and return null or a sensible default on failure. User URLs, tab URLs, and external URLs can all be malformed.
Incorrect
// Throws on invalid URL
function getHost(urlString: string): string {
const url = new URL(urlString);
return url.hostname;
}
// No error handling
const tabUrl = new URL(tab.url);Correct
// Safe URL parsing with null return
function parseURL(url: string): URL | null {
try {
return new URL(url);
} catch (err) {
return null;
}
}
// Get host with fallback
function getURLHostOrProtocol(url: string): string {
try {
const parsed = new URL(url);
if (parsed.host) {
return parsed.host;
}
// Handle special URLs like chrome://extensions
return parsed.protocol.replace(':', '');
} catch (err) {
return url;
}
}
// Usage with cache
const parsedURLCache = new Map<string, URL | null>();
function parseURLWithCache(url: string): URL | null {
if (parsedURLCache.has(url)) {
return parsedURLCache.get(url)!;
}
const result = parseURL(url);
parsedURLCache.set(url, result);
return result;
}Common Failure Cases
// These all throw on `new URL()`:
new URL(''); // Empty string
new URL('not-a-url'); // No protocol
new URL('chrome://newtab'); // Works, but no hostname
new URL('about:blank'); // Works, but no hostname
new URL('javascript:void(0)'); // JavaScript protocol
new URL('file:///local/path'); // File protocol
// Guard clauses for special URLs
function shouldProcessURL(url: string): boolean {
if (!url) return false;
const parsed = parseURL(url);
if (!parsed) return false;
// Skip special protocols
const skipProtocols = ['chrome:', 'chrome-extension:', 'about:', 'javascript:'];
if (skipProtocols.some((p) => parsed.protocol === p)) {
return false;
}
return true;
}Match Pattern Validation
// Validate extension match patterns
function isValidMatchPattern(pattern: string): boolean {
try {
// Match pattern format: <scheme>://<host>/<path>
const match = pattern.match(/^(\*|https?|file|ftp):\/\/([^/]+)\/(.*)$/);
if (!match) return false;
const [, scheme, host] = match;
// Validate host part
if (host !== '*' && !host.match(/^(\*\.)?[a-z0-9.-]+$/i)) {
return false;
}
return true;
} catch (err) {
return false;
}
}Why This Matters
- Tab URLs: Browser tabs can have any URL including malformed ones
- User input: Site lists and custom URLs from users need validation
- Special pages: Chrome pages, about:blank, etc. don't have normal hosts
- Robustness: Extension should never crash on unexpected URL
Validation returns errors array
Validation functions should return an object with errors array and corrected values, not throw exceptions. This allows callers to decide how to handle errors and enables partial recovery.
Incorrect
// Throws on first error - all or nothing
function validateSettings(settings: unknown): UserSettings {
if (typeof settings !== 'object') {
throw new Error('Settings must be an object');
}
if (typeof settings.enabled !== 'boolean') {
throw new Error('enabled must be boolean');
}
// ...
return settings as UserSettings;
}
// Caller can't recover
try {
const settings = validateSettings(input);
} catch (err) {
// Lost all the valid properties
settings = DEFAULT_SETTINGS;
}Correct
interface ValidationResult<T> {
value: T;
errors: string[];
}
function validateSettings(
input: unknown
): ValidationResult<Partial<UserSettings>> {
const errors: string[] = [];
const settings: Partial<UserSettings> = {};
if (typeof input !== 'object' || input === null) {
return { value: {}, errors: ['Settings must be an object'] };
}
const obj = input as Record<string, unknown>;
// Validate each property, collecting errors
if ('enabled' in obj) {
if (typeof obj.enabled === 'boolean') {
settings.enabled = obj.enabled;
} else {
errors.push(`Invalid enabled value: ${obj.enabled}`);
}
}
if ('brightness' in obj) {
const brightness = Number(obj.brightness);
if (!isNaN(brightness) && brightness >= 0 && brightness <= 200) {
settings.brightness = brightness;
} else {
errors.push(`Invalid brightness value: ${obj.brightness}`);
}
}
// ... validate other properties
return { value: settings, errors };
}
// Caller can handle partial success
const { value, errors } = validateSettings(imported);
if (errors.length > 0) {
logWarn('Settings validation errors:', errors);
}
// Apply valid properties, keep defaults for invalid ones
const settings = { ...DEFAULT_SETTINGS, ...value };Validator Helper Pattern
type Validator<T> = (value: unknown) => value is T;
function validateProperty<T>(
obj: Record<string, unknown>,
key: string,
validator: Validator<T>,
fallback: T,
errors: string[
): T {
if (!(key in obj)) {
return fallback;
}
if (validator(obj[key])) {
return obj[key] as T;
}
errors.push(`Unexpected value for "${key}": ${obj[key]}`);
return fallback;
}
// Type guards
const isBoolean = (x: unknown): x is boolean => typeof x === 'boolean';
const isString = (x: unknown): x is string => typeof x === 'string';
const isNumber = (x: unknown): x is number => typeof x === 'number' && !isNaN(x);
// Usage
function validateTheme(input: unknown): ValidationResult<Theme> {
const errors: string[] = [];
const obj = (input ?? {}) as Record<string, unknown>;
return {
value: {
mode: validateProperty(obj, 'mode', isNumber, 1, errors),
brightness: validateProperty(obj, 'brightness', isNumber, 100, errors),
contrast: validateProperty(obj, 'contrast', isNumber, 100, errors),
grayscale: validateProperty(obj, 'grayscale', isNumber, 0, errors),
sepia: validateProperty(obj, 'sepia', isNumber, 0, errors),
},
errors,
};
}Why This Matters
- Partial recovery: Valid properties are preserved even if some fail
- Error aggregation: All errors reported at once, not just the first
- User feedback: Can show all validation issues in UI
- Flexible handling: Caller decides whether to warn, reject, or accept
Avoid Accidental Closure Memory Leaks
Closures capture their enclosing scope. If a long-lived callback references a large object from an outer scope, that object stays in memory for the lifetime of the callback.
Incorrect (closure retains large data):
// content.js - processedData stays in memory forever
function processPage() {
const processedData = extractAllData(); // 10MB of page data
chrome.runtime.onMessage.addListener((message) => {
// This closure captures entire scope including processedData
if (message.type === 'get-summary') {
return processedData.summary; // Only need summary, but entire object retained
}
});
}Correct (capture only needed values):
// content.js - Only summary stays in memory
function processPage() {
const processedData = extractAllData(); // 10MB of page data
const summary = processedData.summary; // Extract what we need
// processedData can be garbage collected
chrome.runtime.onMessage.addListener((message) => {
if (message.type === 'get-summary') {
return summary; // Only 1KB captured
}
});
}Alternative (nullify after use):
// content.js - Explicitly release reference
function processPage() {
let processedData = extractAllData();
// Process immediately
const results = transformData(processedData);
sendResults(results);
// Allow garbage collection
processedData = null;
}Closure leak in event handlers:
// Incorrect - largeData retained
function setup() {
const largeData = generateLargeDataset();
element.onclick = () => console.log(largeData.length);
}
// Correct - extract needed value
function setup() {
const largeData = generateLargeDataset();
const length = largeData.length;
element.onclick = () => console.log(length);
}Reference: Closures MDN
Avoid Holding References to Detached DOM Nodes
When a DOM element is removed from the page but your JavaScript still references it, the element (and its entire subtree) cannot be garbage collected. This is a common source of memory leaks in content scripts.
Incorrect (holds reference to removed elements):
// content.js - Elements stay in memory forever
const elementsCache = new Map();
function cacheElement(id) {
const element = document.getElementById(id);
elementsCache.set(id, element); // Holds reference
}
cacheElement('sidebar');
// Later, page removes #sidebar via SPA navigation
// Element tree stays in memory because elementsCache holds referenceCorrect (use WeakRef or re-query):
// content.js - WeakRef allows garbage collection
const elementsCache = new Map();
function cacheElement(id) {
const element = document.getElementById(id);
if (element) {
elementsCache.set(id, new WeakRef(element));
}
}
function getElement(id) {
const weakRef = elementsCache.get(id);
if (!weakRef) return null;
const element = weakRef.deref();
if (!element || !document.contains(element)) {
elementsCache.delete(id); // Clean up stale reference
return null;
}
return element;
}Alternative (re-query pattern):
// content.js - Query fresh each time
const selectors = {
sidebar: '#sidebar',
header: '.main-header'
};
function getElement(name) {
return document.querySelector(selectors[name]);
}
// No cached references, always gets current element
const sidebar = getElement('sidebar');
if (sidebar) {
processSidebar(sidebar);
}Detecting detached nodes: Use Chrome DevTools → Memory → Take heap snapshot → Search for "Detached"
Reference: Fix Memory Problems
Clean Up Event Listeners When Content Script Unloads
Event listeners attached to page elements persist even after your content script context is destroyed. These orphaned listeners accumulate memory and can cause unexpected behavior.
Incorrect (listeners never removed):
// content.js - Listeners accumulate on SPA navigation
document.addEventListener('scroll', handleScroll);
document.addEventListener('click', handleClick);
window.addEventListener('resize', handleResize);
const targetElement = document.querySelector('.target');
targetElement.addEventListener('mouseenter', showTooltip);
// On SPA navigation, new content script runs, old listeners remainCorrect (cleanup on unload):
// content.js - Track and remove listeners
const listeners = [];
function addTrackedListener(target, event, handler) {
target.addEventListener(event, handler);
listeners.push({ target, event, handler });
}
addTrackedListener(document, 'scroll', handleScroll);
addTrackedListener(document, 'click', handleClick);
addTrackedListener(window, 'resize', handleResize);
// Clean up when content script context is invalidated
window.addEventListener('unload', cleanup);
chrome.runtime.onMessage.addListener((message) => {
if (message.type === 'cleanup') cleanup();
});
function cleanup() {
listeners.forEach(({ target, event, handler }) => {
target.removeEventListener(event, handler);
});
listeners.length = 0;
}AbortController pattern (modern approach):
// content.js - Single signal aborts all listeners
const controller = new AbortController();
const { signal } = controller;
document.addEventListener('scroll', handleScroll, { signal });
document.addEventListener('click', handleClick, { signal });
window.addEventListener('resize', handleResize, { signal });
// Clean up all at once
window.addEventListener('unload', () => controller.abort());MutationObserver cleanup:
// content.js
const observer = new MutationObserver(handleMutations);
observer.observe(document.body, { childList: true, subtree: true });
window.addEventListener('unload', () => {
observer.disconnect();
});Reference: Memory Management MDN
Clear Intervals and Timeouts on Cleanup
Uncleaned intervals continue running after content script context is destroyed, causing errors and wasted CPU. Always clear timers when your script unloads.
Incorrect (interval runs forever):
// content.js - Never cleared
setInterval(() => {
const element = document.querySelector('.dynamic-content');
updateElement(element); // Error after SPA navigation
}, 1000);
setTimeout(() => {
heavyComputation(); // Runs even if no longer needed
}, 60000);Correct (tracked and cleared):
// content.js - Track all timers
const timers = {
intervals: [],
timeouts: []
};
function setTrackedInterval(callback, delay) {
const id = setInterval(callback, delay);
timers.intervals.push(id);
return id;
}
function setTrackedTimeout(callback, delay) {
const id = setTimeout(callback, delay);
timers.timeouts.push(id);
return id;
}
setTrackedInterval(() => {
const element = document.querySelector('.dynamic-content');
if (element) updateElement(element);
}, 1000);
// Cleanup function
function cleanup() {
timers.intervals.forEach(clearInterval);
timers.timeouts.forEach(clearTimeout);
timers.intervals = [];
timers.timeouts = [];
}
window.addEventListener('unload', cleanup);AbortSignal pattern (for newer APIs):
// content.js - Using AbortSignal with setTimeout (proposal)
const controller = new AbortController();
// For fetch requests with timeout
const timeoutId = setTimeout(() => controller.abort(), 5000);
fetch(url, { signal: controller.signal })
.then(response => response.json())
.finally(() => clearTimeout(timeoutId));Service worker note: In service workers, setTimeout/setInterval are unreliable due to termination. Use chrome.alarms instead for persistent timing needs.
Reference: WindowOrWorkerGlobalScope.clearInterval()
Use WeakMap and WeakSet for DOM Element References
When caching data associated with DOM elements, use WeakMap instead of Map. WeakMap allows elements to be garbage collected when removed from the page, preventing memory leaks.
Incorrect (Map prevents garbage collection):
// content.js - Elements never garbage collected
const elementData = new Map();
document.querySelectorAll('.item').forEach(element => {
elementData.set(element, {
originalColor: element.style.color,
processedAt: Date.now()
});
});
// When elements removed from DOM, Map still holds references
// elementData grows unboundedly on SPAsCorrect (WeakMap allows garbage collection):
// content.js - Elements can be garbage collected
const elementData = new WeakMap();
document.querySelectorAll('.item').forEach(element => {
elementData.set(element, {
originalColor: element.style.color,
processedAt: Date.now()
});
});
// When elements removed from DOM, WeakMap entries are automatically cleanedWeakSet for tracking processed elements:
// content.js - Track without preventing GC
const processedElements = new WeakSet();
function processNewElements() {
document.querySelectorAll('.item').forEach(element => {
if (processedElements.has(element)) return; // Skip already processed
processElement(element);
processedElements.add(element);
});
}
// Call on mutations
const observer = new MutationObserver(processNewElements);
observer.observe(document.body, { childList: true, subtree: true });WeakMap/WeakSet limitations:
- Keys must be objects (not strings/numbers)
- Not iterable (can't loop over entries)
- No
.sizeproperty - Use regular Map/Set when you need these features
Reference: WeakMap MDN
Avoid Broadcasting Messages to All Tabs
Sending messages to all tabs wastes resources when only specific tabs need the update. Query for relevant tabs and target messages specifically.
Incorrect (message sent to every tab):
// background.js - Broadcasts to all tabs
async function notifySettingsChange(settings) {
const tabs = await chrome.tabs.query({}); // All tabs
// Sends message to 50+ tabs, most don't care
for (const tab of tabs) {
chrome.tabs.sendMessage(tab.id, {
type: 'settings-updated',
settings
}).catch(() => {}); // Many will fail (no content script)
}
}Correct (targeted messaging):
// background.js - Message only relevant tabs
async function notifySettingsChange(settings) {
// Query only tabs where content script is active
const tabs = await chrome.tabs.query({
url: ['https://github.com/*', 'https://gitlab.com/*']
});
await Promise.all(
tabs.map(tab =>
chrome.tabs.sendMessage(tab.id, {
type: 'settings-updated',
settings
}).catch(() => {}) // Tab might have navigated away
)
);
}Alternative (track interested tabs):
// background.js - Registry pattern
const subscribedTabs = new Set();
chrome.runtime.onMessage.addListener((message, sender) => {
if (message.type === 'subscribe') {
subscribedTabs.add(sender.tab.id);
}
if (message.type === 'unsubscribe') {
subscribedTabs.delete(sender.tab.id);
}
});
chrome.tabs.onRemoved.addListener((tabId) => {
subscribedTabs.delete(tabId);
});
async function notifySubscribers(data) {
for (const tabId of subscribedTabs) {
chrome.tabs.sendMessage(tabId, data).catch(() => {
subscribedTabs.delete(tabId);
});
}
}When broadcast IS acceptable:
- Critical security updates that affect all contexts
- Extension-wide state changes (like disable/enable)
Reference: chrome.tabs.query
Always Check chrome.runtime.lastError in Callbacks
Chrome sets chrome.runtime.lastError when API calls fail (e.g., tab closed, SW not ready). Failing to check it causes silent failures and uncaught error warnings in the console.
Incorrect (silent failures, uncaught errors):
// popup.js - Fails silently if tab doesn't exist
chrome.tabs.sendMessage(tabId, { type: 'get-data' }, (response) => {
// If tab closed, response is undefined but no error handling
displayData(response.data); // TypeError: Cannot read 'data' of undefined
});
// background.js - Warning floods console
chrome.tabs.query({ active: true }, (tabs) => {
chrome.tabs.sendMessage(tabs[0].id, { action: 'ping' }, (response) => {
// "Unchecked runtime.lastError: Could not establish connection"
console.log('Got response:', response);
});
});Correct (proper error handling):
// popup.js - Check lastError before using response
chrome.tabs.sendMessage(tabId, { type: 'get-data' }, (response) => {
if (chrome.runtime.lastError) {
console.warn('Message failed:', chrome.runtime.lastError.message);
showErrorState('Cannot connect to page');
return;
}
displayData(response.data);
});With async/await (try-catch):
// popup.js - Promise-based error handling
async function sendMessageToTab(tabId, message) {
try {
const response = await chrome.tabs.sendMessage(tabId, message);
return response;
} catch (error) {
console.warn('Message failed:', error.message);
return null;
}
}
const data = await sendMessageToTab(tabId, { type: 'get-data' });
if (data) {
displayData(data);
} else {
showErrorState('Page not available');
}Common lastError causes:
- Tab closed or navigated away
- Content script not injected on target page
- Service worker terminated mid-operation
- Extension context invalidated
Reference: chrome.runtime.lastError
Debounce High-Frequency Events Before Messaging
Events like scroll, resize, and mousemove fire dozens of times per second. Sending a message for each event overwhelms the message channel and wastes CPU on redundant processing.
Incorrect (message storm from every event):
// content.js - 60+ messages per second during scroll
window.addEventListener('scroll', () => {
chrome.runtime.sendMessage({
type: 'scroll',
position: window.scrollY
});
});
// User scrolls for 5 seconds = 300+ messagesCorrect (debounced messaging):
// content.js - Debounce to reduce message frequency
function debounce(fn, delay) {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
}
const sendScrollPosition = debounce((position) => {
chrome.runtime.sendMessage({
type: 'scroll',
position
});
}, 100); // Max 10 messages/second
window.addEventListener('scroll', () => {
sendScrollPosition(window.scrollY);
});Throttle for continuous updates:
// content.js - Throttle for regular sampling
function throttle(fn, limit) {
let lastCall = 0;
return (...args) => {
const now = Date.now();
if (now - lastCall >= limit) {
lastCall = now;
fn(...args);
}
};
}
const trackMousePosition = throttle((x, y) => {
chrome.runtime.sendMessage({ type: 'mouse', x, y });
}, 200); // At most every 200ms
document.addEventListener('mousemove', (e) => {
trackMousePosition(e.clientX, e.clientY);
});When to use each:
- Debounce: Final value matters (search input, resize end)
- Throttle: Regular sampling matters (scroll position, animations)
Reference: Debouncing and Throttling Explained
Minimize Message Payload Size
Messages between extension contexts are JSON-serialized. Large payloads increase serialization time and memory usage. Send only the data needed, not entire objects or DOM snapshots.
Incorrect (sending entire objects with unused data):
// content.js - Sends much more than needed
const pageData = {
url: location.href,
title: document.title,
html: document.documentElement.outerHTML, // 500KB+
cookies: document.cookie,
allLinks: Array.from(document.querySelectorAll('a')).map(a => ({
href: a.href,
text: a.textContent,
classList: Array.from(a.classList),
dataset: { ...a.dataset },
rect: a.getBoundingClientRect() // Non-serializable triggers error
}))
};
chrome.runtime.sendMessage({ type: 'page-data', data: pageData });Correct (send only required fields):
// content.js - Minimal payload for the use case
const relevantLinks = Array.from(document.querySelectorAll('a.product-link'))
.slice(0, 50) // Limit quantity
.map(a => ({
href: a.href,
price: a.dataset.price
}));
chrome.runtime.sendMessage({
type: 'page-data',
url: location.href,
links: relevantLinks
});For large data transfers:
// content.js - Use storage for large payloads
async function sendLargeData(data) {
const key = `temp-${Date.now()}`;
await chrome.storage.local.set({ [key]: data });
await chrome.runtime.sendMessage({ type: 'data-ready', key });
}
// background.js
chrome.runtime.onMessage.addListener(async (message) => {
if (message.type === 'data-ready') {
const { [message.key]: data } = await chrome.storage.local.get(message.key);
await processData(data);
await chrome.storage.local.remove(message.key); // Clean up
}
});Non-serializable types to avoid: Functions, DOM elements, Map/Set (use arrays), circular references
Reference: Message Passing
Use Port Connections for Frequent Message Exchange
sendMessage creates overhead for each message. For frequent bidirectional communication between service worker and content scripts, use chrome.runtime.connect to establish a persistent port connection.
Incorrect (new connection per message):
// content.js - High-frequency updates
setInterval(async () => {
const position = getScrollPosition();
// New message channel created each time
await chrome.runtime.sendMessage({
type: 'scroll-position',
position
});
}, 100); // 10 messages/second, 10 connection setups/secondCorrect (reuse port connection):
// content.js - Single persistent connection
const port = chrome.runtime.connect({ name: 'scroll-tracker' });
setInterval(() => {
const position = getScrollPosition();
port.postMessage({ // Reuses existing connection
type: 'scroll-position',
position
});
}, 100);
port.onDisconnect.addListener(() => {
// Reconnect if SW restarts
reconnect();
});// background.js - Handle port connections
chrome.runtime.onConnect.addListener((port) => {
if (port.name === 'scroll-tracker') {
port.onMessage.addListener((message) => {
if (message.type === 'scroll-position') {
processScrollData(message.position);
}
});
}
});When to use each pattern:
sendMessage: One-off requests, infrequent communicationconnect: Streaming data, chat-like interactions, frequent updates- Port connections keep the service worker alive while connected
Reference: Message Passing - Long-lived Connections
Avoid Modifying Content Security Policy Headers
Stripping or weakening Content Security Policy headers breaks site security and can cause functionality issues. If you must modify CSP, do so surgically for specific use cases.
Incorrect (removes all CSP protection):
[
{
"id": 1,
"priority": 1,
"action": {
"type": "modifyHeaders",
"responseHeaders": [
{ "header": "Content-Security-Policy", "operation": "remove" }
]
},
"condition": {
"urlFilter": "*",
"resourceTypes": ["main_frame"]
}
}
]Correct (minimal, targeted modification):
[
{
"id": 1,
"priority": 1,
"action": {
"type": "modifyHeaders",
"responseHeaders": [
{
"header": "Content-Security-Policy",
"operation": "set",
"value": "script-src 'self' https://trusted-cdn.example.com; default-src 'self'"
}
]
},
"condition": {
"urlFilter": "||specific-site.com",
"resourceTypes": ["main_frame"]
}
}
]Better approach (inject content script instead):
// content.js - Work within CSP constraints
// Instead of injecting <script> tags that CSP blocks,
// use content script messaging
// Incorrect - blocked by CSP
const script = document.createElement('script');
script.src = 'https://external.com/script.js';
document.head.appendChild(script);
// Correct - content script approach
chrome.runtime.sendMessage({ type: 'fetch-data' }, (response) => {
processData(response);
});When CSP modification IS acceptable:
- Developer tools that need to debug CSP issues
- Explicit user opt-in for specific sites
- Enterprise deployments with IT approval
Reference: Content Security Policy
Request Minimal Required Permissions
Over-requesting permissions is the #1 rejection reason in Chrome Web Store review. Request only what your extension actually needs, and prefer activeTab over broad host permissions.
Incorrect (over-requesting):
{
"permissions": [
"tabs",
"history",
"bookmarks",
"storage",
"webRequest",
"webRequestBlocking"
],
"host_permissions": [
"<all_urls>"
]
}Correct (minimal permissions):
{
"permissions": [
"activeTab",
"storage"
]
}Use optional_permissions for advanced features:
{
"permissions": ["storage"],
"optional_permissions": ["tabs", "history"],
"optional_host_permissions": ["https://api.example.com/*"]
}// popup.js - Request when user enables feature
async function enableHistoryFeature() {
const granted = await chrome.permissions.request({
permissions: ['history']
});
if (granted) {
initializeHistoryFeature();
}
}Permission alternatives:
| Instead of | Use |
|---|---|
<all_urls> | activeTab + programmatic injection |
tabs (for URL access) | activeTab |
webRequest + webRequestBlocking | declarativeNetRequest |
| Broad host permissions | Specific domains or optional_host_permissions |
Permissions that show warnings:
- Host permissions show "Read and change your data on..."
history,bookmarks,downloadsshow specific warningsactiveTabshows NO warning
Reference: Declare Permissions
Use activeTab Permission Instead of Broad Host Permissions
The activeTab permission grants temporary access to the current tab when the user clicks your extension icon. It shows no install warning and works on any site without requesting <all_urls>.
Incorrect (scary permission warning):
{
"host_permissions": ["<all_urls>"],
"permissions": ["scripting"]
}User sees: "Read and change all your data on all websites"
Correct (no warning, same functionality):
{
"permissions": ["activeTab", "scripting"]
}// background.js - Inject when user clicks icon
chrome.action.onClicked.addListener(async (tab) => {
// activeTab grants temporary access to this specific tab
await chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ['content.js']
});
});What activeTab grants:
- Temporary host permission for the active tab
- Access to tab's URL, title, and favicon
- Ability to inject scripts/CSS into that tab
- Permission expires when tab navigates or closes
Combining with user gestures:
// popup.js - User explicitly clicks button
document.getElementById('extract-btn').addEventListener('click', async () => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
// activeTab permission is active due to popup interaction
const results = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => document.title
});
displayResult(results[0].result);
});When host_permissions ARE needed:
- Background processing without user interaction
- Modifying requests via declarativeNetRequest
- Content scripts that must run on page load
Reference: activeTab Permission
Use declarativeNetRequest Instead of webRequest for Blocking
The webRequest API intercepts every request in JavaScript, adding latency. declarativeNetRequest uses browser-native rule matching that's significantly faster and doesn't require a persistent background page.
Incorrect (JavaScript intercepts every request):
// background.js - Runs JS for every network request
chrome.webRequest.onBeforeRequest.addListener(
(details) => {
if (details.url.includes('ads.example.com')) {
return { cancel: true };
}
if (details.url.includes('tracker.example.com')) {
return { cancel: true };
}
},
{ urls: ['<all_urls>'] },
['blocking']
);
// Every request wakes SW, runs JS, adds latencyCorrect (browser-native rule matching):
{
"permissions": ["declarativeNetRequest"],
"declarative_net_request": {
"rule_resources": [{
"id": "ruleset_1",
"enabled": true,
"path": "rules.json"
}]
}
}[
{
"id": 1,
"priority": 1,
"action": { "type": "block" },
"condition": {
"urlFilter": "ads.example.com",
"resourceTypes": ["script", "image", "xmlhttprequest"]
}
},
{
"id": 2,
"priority": 1,
"action": { "type": "block" },
"condition": {
"urlFilter": "tracker.example.com",
"resourceTypes": ["script", "xmlhttprequest"]
}
}
]Dynamic rules when needed:
// background.js - Update rules programmatically
await chrome.declarativeNetRequest.updateDynamicRules({
addRules: [{
id: 100,
priority: 1,
action: { type: 'block' },
condition: { urlFilter: userBlockedDomain }
}],
removeRuleIds: [99]
});When webRequest IS still needed:
- Modifying request headers dynamically
- Logging requests for debugging
- Complex conditional logic
Reference: Migrate to declarativeNetRequest
Avoid Storing Large Binary Blobs in chrome.storage
chrome.storage JSON-serializes all data. Binary data like images or files become base64-encoded, increasing size by ~33%. Use IndexedDB or Cache API for binary data.
Incorrect (base64-encoded images in storage):
// background.js - Images bloat storage
async function cacheScreenshot(tabId) {
const dataUrl = await chrome.tabs.captureVisibleTab();
// 1MB image becomes ~1.3MB base64 string
await chrome.storage.local.set({
[`screenshot:${tabId}`]: dataUrl
});
}
// 10 screenshots = 13MB, quota nearly exhaustedCorrect (use IndexedDB for binary data):
// background.js - Binary data in IndexedDB
const DB_NAME = 'extension-cache';
const STORE_NAME = 'screenshots';
async function openDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
event.target.result.createObjectStore(STORE_NAME);
};
});
}
async function cacheScreenshot(tabId) {
const dataUrl = await chrome.tabs.captureVisibleTab();
// Convert to Blob for efficient storage
const response = await fetch(dataUrl);
const blob = await response.blob();
const db = await openDatabase();
const tx = db.transaction(STORE_NAME, 'readwrite');
tx.objectStore(STORE_NAME).put(blob, `screenshot:${tabId}`);
}
async function getScreenshot(tabId) {
const db = await openDatabase();
const tx = db.transaction(STORE_NAME, 'readonly');
return new Promise((resolve) => {
const request = tx.objectStore(STORE_NAME).get(`screenshot:${tabId}`);
request.onsuccess = () => resolve(request.result);
});
}When to use each storage:
| Data Type | Best Storage |
|---|---|
| JSON config/settings | chrome.storage |
| Small strings (<100KB) | chrome.storage |
| Images, files, audio | IndexedDB |
| HTTP responses | CacheStorage |
Reference: IndexedDB API
Batch Storage Operations Instead of Individual Calls
Each chrome.storage call has async overhead. Reading or writing multiple values in separate calls multiplies this overhead. Use object syntax to batch operations.
Incorrect (separate call per value):
// background.js - 5 separate async operations
async function saveUserPreferences(prefs) {
await chrome.storage.local.set({ theme: prefs.theme });
await chrome.storage.local.set({ fontSize: prefs.fontSize });
await chrome.storage.local.set({ language: prefs.language });
await chrome.storage.local.set({ notifications: prefs.notifications });
await chrome.storage.local.set({ autoSave: prefs.autoSave });
}
async function loadUserPreferences() {
const theme = await chrome.storage.local.get('theme');
const fontSize = await chrome.storage.local.get('fontSize');
const language = await chrome.storage.local.get('language');
// 5 round trips to storage
}Correct (single batched operation):
// background.js - Single async operation
async function saveUserPreferences(prefs) {
await chrome.storage.local.set({
theme: prefs.theme,
fontSize: prefs.fontSize,
language: prefs.language,
notifications: prefs.notifications,
autoSave: prefs.autoSave
});
}
async function loadUserPreferences() {
const prefs = await chrome.storage.local.get([
'theme', 'fontSize', 'language', 'notifications', 'autoSave'
]);
return prefs;
}Get all stored data at once:
// Get everything (use sparingly for large datasets)
const allData = await chrome.storage.local.get(null);
// Get with defaults
const settings = await chrome.storage.local.get({
theme: 'light', // Default if not set
fontSize: 14,
language: 'en'
});Note: Batching is especially important in content scripts where message passing adds additional latency to each operation.
Reference: chrome.storage API
Boolean variable naming
Prefix boolean variables with is, has, was, did, should, or can to make their boolean nature explicit.
Prefix Guide
| Prefix | Use Case | Example |
|---|---|---|
is | Current state | isEnabled, isActive, isFirefox |
has | Possession/capability | hasPermission, hasCache, hasError |
was | Past state | wasEnabledOnLastCheck, wasModified |
did | Past action | didSlideIn, didInitialize |
should | Conditional behavior | shouldUpdate, shouldInject |
can | Capability | canChangeSettings, canAccessTab |
Incorrect
const enabled = true;
const active = false;
const darkMode = true;
const initialized = false;Correct
const isEnabled = true;
const isActive = false;
const isDarkThemeDetected = true;
const didInitialize = false;
// In interfaces
interface BodyState {
isOpen: boolean;
didNewsSlideIn: boolean;
hasUnreadNews: boolean;
}
// Function parameters
function setStatus(isEnabled: boolean): void { }
// Return types
function checkTheme(): { isDark: boolean; isSystemPreference: boolean } { }Why This Matters
- Self-documenting: Variable name tells you it's a boolean
- Natural reading:
if (isEnabled)reads like English - IDE autocomplete: Type
isto find all boolean state variables - Avoids confusion:
enabledcould be a function,isEnabledis clearly a boolean
Related skills
How it compares
Pick chrome-extension over generic frontend skills when the codebase is specifically a Chrome Manifest V3 browser extension requiring platform-specific performance rules.
FAQ
How many rules does chrome-extension include?
chrome-extension includes 40+ performance rules organized into 8 categories for Chrome Manifest V3 extensions. Rules are prioritized from critical service worker lifecycle issues to incremental API usage improvements, each with explanations and examples.
Does chrome-extension support Manifest V2?
chrome-extension targets Chrome Manifest V3 exclusively. The skill helps agents generate MV3-compliant code and refactor legacy Manifest V2 patterns toward service workers, updated permissions, and modern content script APIs.
Is Chrome Extension safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.