The React Scan browser extension lets you analyze any React application without modifying its code. Install it once and use it across all your React projects.
Installation
Install the React Scan extension from your browser’s extension store:
The extension works on any website running React, including production sites. However, performance monitoring may be limited on production builds.
How It Works
The browser extension consists of three main components:
Background Script
Manages extension state and script injection:
// From background/index.ts
import browser from 'webextension-polyfill';
import { isInternalUrl } from '~utils/helpers';
import { IconState, updateIconForTab } from './icon';
const injectScripts = async (tabId: number) => {
await browser.scripting.executeScript({
target: { tabId },
files: ['src/content/index.js', 'src/inject/index.js'],
});
await browser.tabs.sendMessage(tabId, {
type: 'react-scan:page-reload',
});
};
The background script:
- Injects React Scan into pages
- Manages icon state (enabled/disabled)
- Handles tab switches and page loads
- Filters internal browser URLs
Content Script
Runs in the page context and bridges the extension with the page:
// Content script runs in isolated world
// Communicates between extension and page
browser.runtime.onMessage.addListener((message) => {
if (message.type === 'react-scan:toggle-state') {
// Toggle React Scan state
}
});
Injected Script
Actual React Scan code injected into the page:
// From inject/react-scan.ts
import 'bippy'; // Instruments React
// Extension detection
window.__REACT_SCAN_EXTENSION__ = true;
Extension Architecture
Script Injection Flow
- User navigates to a page
- Background script detects page load
- Checks if URL is valid (not internal browser page)
- Injects content script
- Content script injects React Scan into page
- React Scan initializes and starts monitoring
const init = async (tab: browser.Tabs.Tab) => {
if (!tab.id || !tab.url || isInternalUrl(tab.url)) {
if (tab.id) {
await updateIconForTab(tab, IconState.DISABLED);
}
return;
}
const isLoaded = await isScriptsLoaded(tab.id);
if (!isLoaded) {
await injectScripts(tab.id);
}
};
Internal URL Filtering
The extension skips internal browser pages:
const isInternalUrl = (url: string): boolean => {
return (
url.startsWith('chrome://') ||
url.startsWith('chrome-extension://') ||
url.startsWith('about:') ||
url.startsWith('edge://') ||
url.startsWith('brave://') ||
url.startsWith('opera://')
);
};
Icon States
The extension icon changes based on React Scan status:
enum IconState {
ENABLED = 'enabled', // Green - React Scan active
DISABLED = 'disabled', // Gray - React Scan inactive
}
const updateIconForTab = async (
tab: browser.Tabs.Tab,
state: IconState,
) => {
// Update icon to reflect current state
};
The icon only changes when React Scan successfully initializes on a page with React.
Toggling React Scan
Click the extension icon to toggle React Scan:
browserAction.onClicked.addListener(async (tab) => {
if (!tab.id || !tab.url || isInternalUrl(tab.url)) {
if (tab.id) {
await updateIconForTab(tab, IconState.DISABLED);
}
return;
}
try {
await browser.tabs.sendMessage(tab.id, {
type: 'react-scan:toggle-state',
});
await updateIconForTab(tab, IconState.DISABLED);
} catch {
await updateIconForTab(tab, IconState.DISABLED);
}
});
Tab Management
The extension handles tab lifecycle events:
Tab Updates
browser.tabs.onUpdated.addListener((_tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete') {
void init(tab);
}
});
Tab Activation
browser.tabs.onActivated.addListener(async ({ tabId }) => {
const tab = await browser.tabs.get(tabId);
void init(tab);
});
Window Focus
browser.windows.onFocusChanged.addListener(async (windowId) => {
if (windowId !== browser.windows.WINDOW_ID_NONE) {
const [tab] = await browser.tabs.query({ active: true, windowId });
if (tab) {
void init(tab);
}
}
});
Message Passing
The extension uses message passing for communication:
type BroadcastMessage =
| { type: 'react-scan:is-enabled'; data: { state: boolean } }
| { type: 'react-scan:toggle-state' }
| { type: 'react-scan:page-reload' }
| { type: 'react-scan:ping' };
browser.runtime.onMessage.addListener(
(message: BroadcastMessage, sender: browser.Runtime.MessageSender) => {
if (!sender.tab?.id) return;
if (message.type === 'react-scan:is-enabled') {
void updateIconForTab(
sender.tab,
message.data?.state ? IconState.ENABLED : IconState.DISABLED,
);
}
},
);
Message Types
react-scan:is-enabled - Reports current React Scan state
react-scan:toggle-state - Toggle React Scan on/off
react-scan:page-reload - Page reloaded, reinitialize
react-scan:ping - Check if scripts are loaded
Extension Detection
React Scan code can detect when running as an extension:
if (window.__REACT_SCAN_EXTENSION__) {
// Running as browser extension
// Disable OffscreenCanvas worker (not supported in extensions)
// Use different rendering strategy
}
This allows behavior customization:
// From new-outlines/index.ts
if (
IS_OFFSCREEN_CANVAS_WORKER_SUPPORTED &&
!window.__REACT_SCAN_EXTENSION__ // Skip worker in extension
) {
worker = new Worker(...);
}
Web Workers with OffscreenCanvas don’t work reliably in browser extensions, so React Scan falls back to main thread rendering.
Version Reporting
The extension reports its version:
if (IS_CLIENT && window.__REACT_SCAN_EXTENSION__) {
window.__REACT_SCAN_VERSION__ = ReactScanInternals.version;
}
This helps with:
- Debugging version mismatches
- Compatibility checks
- Feature detection
Permissions
The extension requires these permissions:
{
"permissions": [
"activeTab", // Access to current tab
"scripting", // Script injection
"tabs", // Tab management
"storage" // Settings persistence
],
"host_permissions": [
"<all_urls>" // Access to all websites
]
}
<all_urls> permission is needed to inject React Scan into any page. The extension only activates on pages with React.
Settings Persistence
Extension settings persist using browser.storage:
// Settings are stored per-origin
const key = `react-scan-options-${window.location.origin}`;
browser.storage.local.set({
[key]: {
enabled: true,
showToolbar: true,
// ... other options
},
});
Settings sync across:
- Page reloads
- Tab switches
- Browser restarts
Script Loading Detection
The extension checks if React Scan is already loaded:
const isScriptsLoaded = async (tabId: number): Promise<boolean> => {
try {
await browser.tabs.sendMessage(tabId, { type: 'react-scan:ping' });
return true;
} catch {
return false;
}
};
This prevents:
- Double injection
- Duplicate event listeners
- Memory leaks
Error Handling
The extension gracefully handles errors:
try {
await browser.scripting.executeScript({
target: { tabId },
files: ['src/content/index.js', 'src/inject/index.js'],
});
} catch (e) {
console.error('Script injection error:', e);
// Update icon to show disabled state
}
Common errors:
- Page doesn’t allow script injection (CSP)
- Internal browser page (chrome://, about:)
- Extension permissions revoked
- Tab closed before injection completes
Debugging the Extension
Enable Debug Mode
import { scan } from 'react-scan';
scan({
_debug: 'verbose', // Log internal errors
});
Check Console
Look for React Scan logs:
[React Scan] Invalid options - Configuration errors
[React Scan Internal Error] - Runtime errors
[React Scan] Failed to load - Initialization failures
Inspect Extension
- Open browser extension management
- Enable “Developer mode”
- Click “Inspect” on React Scan extension
- Check console for background script errors
Limitations
Production Builds
React production builds have limited debugging:
- Component names may be minified
- React DevTools hook may be unavailable
- Performance metrics less detailed
Content Security Policy
Strict CSP may block script injection:
Content-Security-Policy: script-src 'self'
The extension cannot inject into pages with restrictive CSP.
Cross-Origin Iframes
By default, React Scan doesn’t run in iframes:
scan({
allowInIframe: true, // Enable for iframe support
});
Best Practices
- Use on development builds - More accurate results with source maps
- Toggle on/off as needed - Extension adds overhead, disable when not debugging
- Check icon state - Green means active, gray means inactive or no React detected
- Review console warnings - React Scan logs helpful debugging information
- Update regularly - Extension auto-updates with new features and fixes