Agents extension-anti-patterns
Common mistakes, performance pitfalls, and store rejection reasons in browser extension development
git clone https://github.com/aRustyDev/agents
T=$(mktemp -d) && git clone --depth=1 https://github.com/aRustyDev/agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/content/plugins/web/browser-extension-dev/skills/extension-anti-patterns" ~/.claude/skills/arustydev-agents-extension-anti-patterns && rm -rf "$T"
content/plugins/web/browser-extension-dev/skills/extension-anti-patterns/SKILL.mdBrowser Extension Anti-Patterns
Common mistakes to avoid when developing browser extensions for Chrome, Firefox, and Safari.
Overview
This skill catalogs anti-patterns that lead to:
- Poor performance and memory leaks
- Store rejections (Chrome Web Store, AMO, Safari App Store)
- Security vulnerabilities
- Cross-browser incompatibilities
- Poor user experience
This skill covers:
- Performance anti-patterns
- Store rejection reasons
- API misuse patterns
- Manifest configuration mistakes
- Content script pitfalls
This skill does NOT cover:
- General JavaScript anti-patterns
- Server-side code issues
- Native messaging host problems
Quick Reference
Red Flags Checklist
| Anti-Pattern | Impact | Solution |
|---|---|---|
permission | Store rejection | Use specific host permissions |
| Blocking background operations | Extension suspend issues | Use async/Promise patterns |
| DOM polling in content scripts | High CPU usage | Use MutationObserver |
| Unbounded storage growth | Memory exhaustion | Implement retention policies |
or | CSP violation, store rejection | Use static code |
Performance Anti-Patterns
1. DOM Polling
Problem: Using
setInterval to check for DOM changes.
// BAD: Polls every 100ms, wastes CPU setInterval(() => { const element = document.querySelector('.target'); if (element) { processElement(element); } }, 100);
Solution: Use MutationObserver.
// GOOD: Only fires when DOM changes const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { const element = document.querySelector('.target'); if (element) { processElement(element); observer.disconnect(); } } }); observer.observe(document.body, { childList: true, subtree: true });
2. Synchronous Storage Access
Problem: Using synchronous storage patterns that block execution.
// BAD: Blocks until storage returns const data = await browser.storage.local.get('key'); // 50+ more sequential awaits...
Solution: Batch storage operations.
// GOOD: Single storage call const data = await browser.storage.local.get(['key1', 'key2', 'key3']);
3. Memory Leaks in Content Scripts
Problem: Event listeners not cleaned up when navigating away.
// BAD: Listener persists after navigation window.addEventListener('scroll', handleScroll);
Solution: Use AbortController or cleanup handlers.
// GOOD: Cleanup on unload const controller = new AbortController(); window.addEventListener('scroll', handleScroll, { signal: controller.signal }); window.addEventListener('beforeunload', () => controller.abort());
4. Large Message Payloads
Problem: Sending large data between background and content scripts.
// BAD: Serializing megabytes of data browser.runtime.sendMessage({ type: 'data', payload: hugeArray });
Solution: Use chunking or IndexedDB for large data.
// GOOD: Store in IndexedDB, pass reference await idb.put('largeData', hugeArray); browser.runtime.sendMessage({ type: 'dataReady', key: 'largeData' });
5. Blocking Service Worker
Problem: Long-running operations in service worker prevent suspension.
// BAD: Service worker can't sleep background.js: while (processing) { await processChunk(); // Runs for minutes... }
Solution: Use alarms for long operations.
// GOOD: Let service worker sleep between chunks browser.alarms.create('processChunk', { delayInMinutes: 0.1 }); browser.alarms.onAlarm.addListener(async (alarm) => { if (alarm.name === 'processChunk') { const done = await processNextChunk(); if (!done) { browser.alarms.create('processChunk', { delayInMinutes: 0.1 }); } } });
Store Rejection Reasons
Chrome Web Store
| Reason | Trigger | Fix |
|---|---|---|
| Broad host permissions | or without justification | Narrow to specific domains |
| Remote code execution | Loading scripts from external URLs | Bundle all code locally |
| Misleading metadata | Description doesn't match functionality | Accurate description |
| Excessive permissions | Requesting unused permissions | Remove unnecessary permissions |
| Privacy violation | Collecting data without disclosure | Add privacy policy |
| Single purpose violation | Multiple unrelated features | Split into separate extensions |
| Affiliate/redirect abuse | Hidden affiliate links | Transparent disclosure |
Firefox Add-ons (AMO)
| Reason | Trigger | Fix |
|---|---|---|
| Obfuscated code | Minified code without source | Submit source code |
| eval() usage | Dynamic code execution | Refactor to static code |
| Missing gecko ID | No browser_specific_settings | Add gecko.id to manifest |
| CSP violations | Inline scripts in HTML | Move to external files |
| Tracking without consent | Analytics without disclosure | Add opt-in consent |
Safari App Store
| Reason | Trigger | Fix |
|---|---|---|
| Missing privacy manifest | iOS 17+ requirement | Add PrivacyInfo.xcprivacy |
| Guideline 2.3 violations | Inaccurate metadata | Match screenshots to functionality |
| Guideline 4.2 violations | Spam/low quality | Add meaningful functionality |
| Missing entitlements | Using APIs without entitlement | Configure in Xcode |
API Misuse Patterns
1. tabs.query Without Filters
Problem: Querying all tabs unnecessarily.
// BAD: Gets ALL tabs across ALL windows const tabs = await browser.tabs.query({});
Solution: Use specific filters.
// GOOD: Only active tab in current window const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
2. executeScript Without Target
Problem: Injecting scripts without specifying target.
// BAD: Injects into wrong tab or fails silently browser.scripting.executeScript({ func: myFunction });
Solution: Always specify target.
// GOOD: Explicit target browser.scripting.executeScript({ target: { tabId: tab.id }, func: myFunction });
3. Ignoring Promise Rejections
Problem: Not handling API errors.
// BAD: Silent failures browser.tabs.sendMessage(tabId, message);
Solution: Handle errors appropriately.
// GOOD: Handle disconnected tabs try { await browser.tabs.sendMessage(tabId, message); } catch (error) { if (error.message.includes('disconnected')) { // Tab closed or navigated away - expected } else { console.error('Unexpected error:', error); } }
4. Storage Without Limits
Problem: Writing unlimited data to storage.
// BAD: Storage grows unbounded const history = await browser.storage.local.get('history'); history.items.push(newItem); // Never removes old items await browser.storage.local.set({ history });
Solution: Implement retention policy.
// GOOD: Limit to last 1000 items const MAX_HISTORY = 1000; const history = await browser.storage.local.get('history'); history.items.push(newItem); if (history.items.length > MAX_HISTORY) { history.items = history.items.slice(-MAX_HISTORY); } await browser.storage.local.set({ history });
Manifest Anti-Patterns
1. Over-Permissioning
// BAD: Requests everything { "permissions": [ "<all_urls>", "tabs", "history", "bookmarks", "downloads", "webRequest", "webRequestBlocking" ] }
// GOOD: Minimum viable permissions { "permissions": ["storage", "activeTab"], "optional_permissions": ["tabs"], "host_permissions": ["*://example.com/*"] }
2. Missing Icons
// BAD: Only one icon size { "icons": { "128": "icon.png" } }
// GOOD: Multiple sizes for different contexts { "icons": { "16": "icons/16.png", "32": "icons/32.png", "48": "icons/48.png", "128": "icons/128.png" } }
3. Insecure CSP
// BAD: Allows unsafe-eval { "content_security_policy": { "extension_pages": "script-src 'self' 'unsafe-eval'; object-src 'self'" } }
// GOOD: Strict CSP { "content_security_policy": { "extension_pages": "script-src 'self'; object-src 'self'" } }
Content Script Pitfalls
1. Global Namespace Pollution
Problem: Variables leak into page scope.
// BAD: Pollutes global namespace var myExtensionData = {}; // Also bad: top-level const/let in non-module scripts const config = {};
Solution: Use IIFE or modules.
// GOOD: IIFE isolation (function() { const myExtensionData = {}; // All code here })(); // BETTER: Use ES modules (MV3) // manifest.json: "content_scripts": [{ "js": ["content.js"], "type": "module" }]
2. Race Conditions with Page Scripts
Problem: Page scripts modify DOM before content script runs.
// BAD: Element may not exist yet or be replaced const button = document.querySelector('.submit'); button.addEventListener('click', handler);
Solution: Wait for element with timeout.
// GOOD: Wait for element with timeout function waitForElement(selector, timeout = 5000) { return new Promise((resolve, reject) => { const element = document.querySelector(selector); if (element) return resolve(element); const observer = new MutationObserver(() => { const element = document.querySelector(selector); if (element) { observer.disconnect(); resolve(element); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); reject(new Error(`Timeout waiting for ${selector}`)); }, timeout); }); }
Cross-Browser Pitfalls
1. Chrome-Only APIs
| Chrome API | Firefox Alternative | Safari Alternative |
|---|---|---|
| Not available | Not available |
| Not available | Not available |
| Partial support | Limited support |
2. Callback vs Promise APIs
// BAD: Chrome callback style chrome.tabs.query({}, function(tabs) { // Works in Chrome, fails in Firefox });
// GOOD: Use webextension-polyfill or browser.* const tabs = await browser.tabs.query({});
Checklist Before Submission
- No
without justification<all_urls> - No
oreval()new Function() - No remote code loading
- No obfuscated/minified code (or source provided)
- Privacy policy if collecting data
- Accurate store description
- Multiple icon sizes
- Gecko ID for Firefox
- Tested on all target browsers
- Storage limits implemented
- Error handling for all API calls