Most developers are content with the standard Elements, Console, and Network tabs. But when I started building complex state-driven applications, I found that digging through the Console to find a specific piece of nested state was a productivity killer. That’s when I realized I needed to learn how to create a Chrome DevTools extension.
Unlike standard browser extensions that modify the page or add a popup, a DevTools extension allows you to embed your own custom panels directly into the Chrome Developer Tools. This means you can create a dedicated UI for debugging your specific application logic, inspecting custom data structures, or triggering internal app events without writing clumsy ‘window.myapp.debug()’ calls in the console.
The Challenge: Bridging the Execution Gap
The biggest hurdle when learning how to create a Chrome DevTools extension is understanding the Execution Context. In a standard extension, you have a background script and content scripts. In a DevTools extension, you have a third layer: the DevTools Page.
The DevTools page is invisible. It doesn’t have a DOM that the user sees. Instead, it acts as a coordinator that creates the actual panels the developer interacts with. If you’ve used the React Developer Tools extension guide, you’ve seen this in action: the ‘Components’ and ‘Profiler’ tabs are essentially custom panels communicating with the page’s JavaScript runtime.
Solution Overview: The Three-Tier Architecture
To build a functional DevTools extension, you need three distinct components working in harmony:
- The Manifest: The blueprint that tells Chrome this is a
devtools_page. - The DevTools Page: The invisible orchestrator that initializes your panels.
- The Panel: The actual HTML/JS interface that appears in the DevTools window.
Because the Panel runs in the context of the DevTools window—not the inspected page—it cannot access the page’s variables directly. To get data from the website, you must use chrome.devtools.inspectedWindow.eval(), which executes code in the context of the page being debugged.
Implementation: Building Your First Custom Panel
Step 1: The Manifest File
Start by creating a manifest.json. Note the devtools_page key; this is the magic ingredient that differentiates this from a standard extension.
{
"manifest_version": 3,
"name": "My Custom Debugger",
"version": "1.0",
"devtools_page": "devtools.html",
"permissions": ["tabs", "activeTab"]
}
Step 2: The DevTools Orchestrator
Create devtools.html and a corresponding devtools.js. The HTML file is just a shell; the JS file is where you define the panel.
devtools.js:
chrome.devtools.panels.create(
"My Debugger",
"icon.png",
"panel.html",
function(panel) {
console.log("Custom panel created successfully!");
}
);
Step 3: The Panel UI and Logic
Now, create panel.html and panel.js. This is where you build your actual tool. To make it useful, let’s implement a function that grabs a global variable from the inspected page.
panel.js:
document.getElementById('get-state').addEventListener('click', () => {
// This code runs in the DevTools context
chrome.devtools.inspectedWindow.eval("window.appState", (result, isException) => {
if (isException) {
document.getElementById('output').innerText = "Error: appState not found";
} else {
document.getElementById('output').innerText = JSON.stringify(result, null, 2);
}
});
});
As shown in the image below, this architecture allows you to maintain a clean separation between your debugging UI and the production code of your website.
Advanced Techniques: Real-time Communication
Using eval() is great for one-off requests, but for a professional tool, you want real-time updates. I’ve found that the most reliable pattern is using a Background Service Worker as a message bus.
The Workflow:
1. Content Script: Listens for state changes in the app and sends a message to the background script using chrome.runtime.sendMessage().
2. Background Script: Receives the data and caches it or forwards it.
3. Panel: Polls the background script or uses chrome.runtime.onMessage to update the UI instantly.
This is exactly how some of the best devtools extensions for debugging JavaScript handle high-frequency data streams without freezing the browser UI.
Common Pitfalls and Performance
In my experience, there are two main traps developers fall into when building these tools:
1. The ‘Context-Confusion’ Bug
Developers often try to use window.location inside panel.js expecting it to be the URL of the inspected page. It isn’t. It’s the internal chrome-extension:// URL. Always use chrome.devtools.inspectedWindow for page-level interactions.
2. Performance Degradation
Running heavy eval() calls or sending massive JSON blobs via sendMessage() every 16ms will lag the entire browser. I recommend throttling updates to 100-200ms or implementing a “Pause/Play” toggle in your panel to stop data streams when not actively debugging.
Case Study: Building a Redux-like State Inspector
I recently used this approach to build a custom inspector for a proprietary state machine. Instead of logging every transition to the console (which creates thousands of entries), I built a DevTools panel that visualized the state as a graph. By using chrome.devtools.panels.create, I could keep the visualization persistent across page refreshes, which is a massive advantage over content-script-based overlays.
If you’re interested in further optimizing your workflow, check out our deeper guides on automation and productivity tools to see how browser extension logic can be applied to wider automation tasks.