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()
Popup Interface
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
- Security
- Use minimal permissions
- Validate data
- Implement CSP
- Handle sensitive data properly
- Performance
- Optimize resource usage
- Use event delegation
- Implement caching
- Monitor performance
- 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.