Building Chrome Extensions V3

Last Modified: January 4, 2025

Chrome's Manifest V3 introduces new patterns for building secure and performant browser extensions. This guide covers everything you need to know to build modern Chrome extensions.

Getting Started with Manifest V3

Let's explore the key components of a modern Chrome extension.

Basic Extension Structure

Create the manifest file:

{
    "manifest_version": 3,
    "name": "My Extension",
    "version": "1.0",
    "description": "A modern Chrome extension",
    "permissions": [
        "storage",
        "activeTab"
    ],
    "action": {
        "default_popup": "popup.html",
        "default_icon": {
            "16": "icons/icon16.png",
            "48": "icons/icon48.png",
            "128": "icons/icon128.png"
        }
    },
    "background": {
        "service_worker": "background.js",
        "type": "module"
    },
    "content_scripts": [{
        "matches": ["<all_urls>"],
        "js": ["content.js"]
    }]
}

Service Worker Implementation

Create a background service worker:

// background.js
let state = {
    enabled: true,
    settings: {}
}

chrome.runtime.onInstalled.addListener(async () => {
    // Initialize extension state
    await chrome.storage.local.set({ state })

    // Create context menu
    chrome.contextMenus.create({
        id: 'myExtension',
        title: 'My Extension',
        contexts: ['selection']
    })
})

chrome.runtime.onMessage.addListener(
    (message, sender, sendResponse) => {
        if (message.type === 'getState') {
            sendResponse({ state })
            return true
        }

        if (message.type === 'setState') {
            state = { ...state, ...message.payload }
            chrome.storage.local.set({ state })
            sendResponse({ success: true })
            return true
        }
    }
)

Content Script Implementation

Implement page interaction logic:

// content.js
class PageManager {
    constructor() {
        this.initialized = false
        this.init()
    }

    async init() {
        if (this.initialized) return

        // Get extension state
        const { state } = await chrome.runtime.sendMessage({
            type: 'getState'
        })

        if (!state.enabled) return

        this.setupListeners()
        this.initialized = true
    }

    setupListeners() {
        document.addEventListener('click', this.handleClick)
        document.addEventListener('keydown', this.handleKeydown)
    }

    handleClick = (event) => {
        if (!event.target.matches('.interactive-element')) return

        chrome.runtime.sendMessage({
            type: 'elementClicked',
            payload: {
                elementId: event.target.id,
                timestamp: Date.now()
            }
        })
    }

    handleKeydown = (event) => {
        if (event.ctrlKey && event.key === 'e') {
            this.toggleExtension()
        }
    }

    async toggleExtension() {
        const { state } = await chrome.runtime.sendMessage({
            type: 'getState'
        })

        await chrome.runtime.sendMessage({
            type: 'setState',
            payload: { enabled: !state.enabled }
        })
    }
}

new PageManager()

Create an interactive popup:

<!-- popup.html -->
<!DOCTYPE html>
<html>
<head>
    <style>
        body {
            width: 300px;
            padding: 16px;
        }
        .settings {
            display: grid;
            gap: 8px;
        }
    </style>
</head>
<body>
    <div class="settings">
        <label>
            <input type="checkbox" id="enableExtension">
            Enable Extension
        </label>
        <button id="saveSettings">Save Settings</button>
    </div>
    <script src="popup.js"></script>
</body>
</html>

// popup.js
class PopupManager {
    constructor() {
        this.enableInput = document.getElementById('enableExtension')
        this.saveButton = document.getElementById('saveSettings')
        this.init()
    }

    async init() {
        const { state } = await chrome.runtime.sendMessage({
            type: 'getState'
        })

        this.enableInput.checked = state.enabled
        this.setupListeners()
    }

    setupListeners() {
        this.saveButton.addEventListener('click', 
            this.handleSave.bind(this)
        )
    }

    async handleSave() {
        await chrome.runtime.sendMessage({
            type: 'setState',
            payload: {
                enabled: this.enableInput.checked
            }
        })

        window.close()
    }
}

new PopupManager()

API Integration

Implement external API communication:

// api-client.js
class APIClient {
    constructor(baseURL) {
        this.baseURL = baseURL
        this.headers = {
            'Content-Type': 'application/json'
        }
    }

    async setAuthToken(token) {
        this.headers.Authorization = `Bearer ${token}`
        await chrome.storage.local.set({ authToken: token })
    }

    async request(endpoint, options = {}) {
        const response = await fetch(
            `${this.baseURL}${endpoint}`,
            {
                ...options,
                headers: {
                    ...this.headers,
                    ...options.headers
                }
            }
        )

        if (!response.ok) {
            throw new Error(
                `API request failed: ${response.statusText}`
            )
        }

        return response.json()
    }
}

// Usage in background.js
const api = new APIClient('https://api.example.com')

chrome.runtime.onMessage.addListener(
    async (message, sender, sendResponse) => {
        if (message.type === 'apiRequest') {
            try {
                const data = await api.request(
                    message.endpoint,
                    message.options
                )
                sendResponse({ data })
            } catch (error) {
                sendResponse({ error: error.message })
            }
            return true
        }
    }
)

Storage Management

Implement efficient storage handling:

class StorageManager {
    constructor() {
        this.cache = new Map()
    }

    async get(key) {
        if (this.cache.has(key)) {
            return this.cache.get(key)
        }

        const result = await chrome.storage.local.get(key)
        const value = result[key]

        if (value) {
            this.cache.set(key, value)
        }

        return value
    }

    async set(key, value) {
        this.cache.set(key, value)
        await chrome.storage.local.set({ [key]: value })
    }

    async remove(key) {
        this.cache.delete(key)
        await chrome.storage.local.remove(key)
    }

    async clear() {
        this.cache.clear()
        await chrome.storage.local.clear()
    }
}

Performance Optimization

Implement efficient resource usage:

class PerformanceMonitor {
    constructor() {
        this.metrics = new Map()
        this.startTime = Date.now()
    }

    track(operation, duration) {
        if (!this.metrics.has(operation)) {
            this.metrics.set(operation, [])
        }

        this.metrics.get(operation).push(duration)

        // Clean up old metrics
        if (this.metrics.get(operation).length > 100) {
            this.metrics.get(operation).shift()
        }
    }

    async measure(operation, fn) {
        const start = performance.now()
        try {
            return await fn()
        } finally {
            const duration = performance.now() - start
            this.track(operation, duration)
        }
    }

    getStats(operation) {
        const metrics = this.metrics.get(operation) || []

        return {
            avg: metrics.reduce((a, b) => a + b, 0) / metrics.length,
            min: Math.min(...metrics),
            max: Math.max(...metrics),
            count: metrics.length
        }
    }
}

Error Handling

Implement robust error handling:

class ErrorHandler {
    constructor() {
        this.setupListeners()
    }

    setupListeners() {
        chrome.runtime.onError.addListener(
            this.handleError.bind(this)
        )

        window.addEventListener(
            'unhandledrejection',
            this.handlePromiseError.bind(this)
        )
    }

    async handleError(error) {
        console.error('Extension error:', error)

        await this.logError({
            type: 'runtime',
            message: error.message,
            stack: error.stack,
            timestamp: Date.now()
        })
    }

    async handlePromiseError(event) {
        console.error('Unhandled promise rejection:', 
            event.reason
        )

        await this.logError({
            type: 'promise',
            message: event.reason.message,
            stack: event.reason.stack,
            timestamp: Date.now()
        })
    }

    async logError(error) {
        const errors = await chrome.storage.local.get('errors')
        const errorLog = errors.errorLog || []

        errorLog.push(error)

        // Keep only last 100 errors
        if (errorLog.length > 100) {
            errorLog.shift()
        }

        await chrome.storage.local.set({ errorLog })
    }
}

Testing

Implement extension testing:

// tests/extension.test.js
describe('Extension', () => {
    beforeEach(async () => {
        await chrome.storage.local.clear()
    })

    test('initialization', async () => {
        const { state } = await chrome.runtime.sendMessage({
            type: 'getState'
        })

        expect(state.enabled).toBe(true)
    })

    test('toggle extension', async () => {
        await chrome.runtime.sendMessage({
            type: 'setState',
            payload: { enabled: false }
        })

        const { state } = await chrome.runtime.sendMessage({
            type: 'getState'
        })

        expect(state.enabled).toBe(false)
    })
})

Best Practices

  1. Security
  • Use minimal permissions
  • Validate data
  • Implement CSP
  • Handle sensitive data properly
  1. Performance
  • Optimize resource usage
  • Use event delegation
  • Implement caching
  • Monitor performance
  1. Development
  • Use TypeScript
  • Implement testing
  • Document code
  • Follow Chrome guidelines

Conclusion

Manifest V3 provides a modern framework for Chrome extensions:

  • Service worker-based background scripts
  • Enhanced security model
  • Better performance
  • Improved reliability

Key takeaways:

  • Follow security best practices
  • Optimize performance
  • Handle errors properly
  • Test thoroughly
  • Monitor usage and errors

By following these patterns and best practices, you can build robust and secure Chrome extensions using Manifest V3.