Chrome Extension Webnavigation Patterns — Best Practices
8 min readWeb Navigation API Patterns
The chrome.webNavigation API provides powerful capabilities for monitoring and analyzing navigation events in Chrome extensions. This document covers advanced patterns for working with this API effectively.
Event Sequence
Navigation events fire in a predictable sequence:
onBeforeNavigate- Fired when navigation is about to occuronCommitted- When the document begins loading (response received)onDOMContentLoaded- DOM is parsed, stylesheets/images may still be loadingonCompleted- Page fully loaded (all resources fetched)
chrome.webNavigation.onCompleted.addListener((details) => {
console.log(`Page loaded: ${details.url} in ${details.timeStamp}`);
});
Frame Tracking
frameId: 0- Main/top-level frameframeId > 0- Subframes (iframes, frames)
chrome.webNavigation.onCommitted.addListener((details) => {
if (details.frameId === 0) {
console.log(`Main frame navigation: ${details.url}`);
} else {
console.log(`Subframe navigation in frame ${details.frameId}: ${details.url}`);
}
});
URL Filtering
Use the filters parameter to receive only events matching specific URLs:
chrome.webNavigation.onCompleted.addListener(
(details) => {
console.log(`Navigation to: ${details.url}`);
},
{ url: [{ urlContains: 'example.com' }, { urlMatches: '.*\\.google\\.com.*' }] }
);
SPA Detection
Single Page Applications (SPAs) don’t trigger full page loads. Use these events:
onHistoryStateUpdated-pushState/replaceStatecalledonReferenceFragmentUpdated- Hash fragment changed
// SPA Router Detector
chrome.webNavigation.onHistoryStateUpdated.addListener((details) => {
console.log(`SPA route change: ${details.url} (transitionType: ${details.transitionType})`);
}, { url: [{ schemes: ['https'] }] });
chrome.webNavigation.onReferenceFragmentUpdated.addListener((details) => {
console.log(`Hash change: ${details.url}`);
});
Error Detection
Track failed navigations:
chrome.webNavigation.onErrorOccurred.addListener((details) => {
console.error(`Navigation error in ${details.frameId === 0 ? 'main' : 'subframe'}: ${details.url}`);
console.error(`Error: ${details.error}`);
});
Tab Navigation Tracking
Combine with the chrome.tabs API for complete navigation tracking:
chrome.webNavigation.onCompleted.addListener(async (details) => {
if (details.frameId === 0) {
const tab = await chrome.tabs.get(details.tabId);
console.log(`Final URL: ${tab.url}, Title: ${tab.title}`);
}
});
Transition Types
The transitionType property indicates how navigation was initiated:
| Type | Description |
|---|---|
link |
Clicked a link |
typed |
URL typed in address bar |
auto_bookmark |
Bookmark or menu selection |
auto_subframe |
Automatic iframe navigation |
manual_subframe |
User-initiated iframe navigation |
generated |
Omnibox suggestion |
startup |
Startup page |
form_submit |
Form submission |
reload |
Page reload |
keyword |
Keyword navigation |
Transition Qualifiers
Additional qualifiers modify the transition:
client_redirect: JavaScript redirectserver_redirect: HTTP redirect (301/302)forward_back: Forward/back button navigationfrom_address_bar: Address bar URL
chrome.webNavigation.onCommitted.addListener((details) => {
const { transitionType, transitionQualifiers } = details;
console.log(`Navigation type: ${transitionType}, qualifiers: ${transitionQualifiers.join(', ')}`);
});
Frame Lifecycle
Detect when new tabs or windows are created via navigation:
chrome.webNavigation.onCreatedNavigationTarget.addListener((details) => {
console.log(`New tab/window created: ${details.url}`);
console.log(`Source tab: ${details.sourceTabId}, Source frame: ${details.sourceFrameId}`);
});
Performance Monitoring
Measure time between navigation events:
const navigationTimes = {};
chrome.webNavigation.onBeforeNavigate.addListener((details) => {
if (details.frameId === 0) {
navigationTimes[details.tabId] = { start: Date.now() };
}
});
chrome.webNavigation.onCompleted.addListener((details) => {
if (details.frameId === 0 && navigationTimes[details.tabId]) {
const duration = Date.now() - navigationTimes[details.tabId].start;
console.log(`Page load time: ${duration}ms`);
delete navigationTimes[details.tabId];
}
});
Conditional Content Script Injection
Inject content scripts based on navigation patterns:
chrome.webNavigation.onCompleted.addListener((details) => {
if (details.frameId === 0 && details.url.includes('example.com')) {
chrome.scripting.executeScript({
target: { tabId: details.tabId },
files: ['content-script.js']
});
}
});
Code Examples
SPA Router Detector
function detectSPANavigation() {
const spRoutes = new Map();
chrome.webNavigation.onHistoryStateUpdated.addListener((details) => {
const tabId = details.tabId;
if (!spRoutes.has(tabId)) {
spRoutes.set(tabId, []);
}
spRoutes.get(tabId).push({ url: details.url, time: details.timeStamp });
console.log(`SPA Route: ${details.url}`);
});
}
Navigation Logger
chrome.webNavigation.onBeforeNavigate.addListener(d => log('beforeNavigate', d));
chrome.webNavigation.onCommitted.addListener(d => log('committed', d));
chrome.webNavigation.onDOMContentLoaded.addListener(d => log('domContentLoaded', d));
chrome.webNavigation.onCompleted.addListener(d => log('completed', d));
function log(event, details) {
console.log(`[${event}] ${details.url} (tab: ${details.tabId}, frame: ${details.frameId})`);
}
Frame-Aware Content Injector
chrome.webNavigation.onCompleted.addListener((details) => {
const Injector = {
injectToMainFrame: (tabId) => {
chrome.scripting.executeScript({
target: { tabId, frameIds: [0] },
func: () => console.log('Injected to main frame')
});
},
injectToAllFrames: (tabId) => {
chrome.scripting.executeScript({
target: { tabId, allFrames: true },
func: () => console.log(`Injected to frame ${window.frameId}`)
});
}
};
details.frameId === 0 ? Injector.injectToMainFrame(details.tabId) : null;
});
See Also
Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.