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:

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);
    }
  });
});
Architecture diagram showing the flow of data between the Inspected Page, DevTools Page, and Custom Panel
Architecture diagram showing the flow of data between the Inspected Page, DevTools Page, and Custom Panel

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.