Chrome Extension Content Script Lifecycle — Best Practices
3 min readContent Script Lifecycle Management
Content scripts in Chrome extensions have a distinct lifecycle that differs from regular web page scripts. Understanding this lifecycle is crucial for building robust, memory-efficient extensions.
Injection Timing
Content scripts can be injected at different points in the page loading process:
| Run At | Description |
|---|---|
document_start |
Before any DOM is constructed |
document_idle |
After DOMContentLoaded (default) |
document_end |
After DOM is fully parsed |
{
"content_scripts": [{
"matches": ["<all_urls>"],
"run_at": "document_idle"
}]
}
Static vs Dynamic Injection
Static Injection (Manifest)
{
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["content.js"]
}]
}
Dynamic Injection (Scripting API)
chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => { console.log('Injected!'); }
});
Multiple Injections & Navigation
Content scripts inject once per page navigation, not on hash changes. For SPAs, you need additional handling:
// SPA-aware content script with MutationObserver
const observer = new MutationObserver((mutations) => {
// Detect route changes in SPA
if (mutations.some(m => m.addedNodes.length > 0)) {
initializeSPAComponents();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
Cleanup on Navigation
Content script globals are garbage collected when the page unloads. Always clean up:
window.addEventListener('unload', () => {
observer?.disconnect();
// Remove event listeners
document.removeEventListener('click', handleClick);
});
Extension Updates
When an extension updates, existing content scripts keep running but lose their connection to the background script:
// Detect stale context
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg === 'ping') sendResponse('pong');
});
// Reconnection logic in background
async function reconnectContentScripts() {
const tabs = await chrome.tabs.query({});
for (const tab of tabs) {
chrome.tabs.sendMessage(tab.id, 'reconnect');
}
}
Cross-Frame Injection
Use all_frames to inject into iframes:
{
"content_scripts": [{
"matches": ["<all_urls>"],
"all_frames": true,
"js": ["content.js"]
}]
}
Memory Leak Prevention
Always clean up to avoid memory leaks:
let observer = null;
let clickHandler = null;
function cleanup() {
observer?.disconnect();
document.removeEventListener('click', clickHandler);
observer = null;
clickHandler = null;
}
window.addEventListener('unload', cleanup);
Related Resources
Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.