Unlocking Chrome DevTools: Architecture, Protocols, and Remote Debugging Techniques
This article explores the components of Chrome DevTools, explains the DevTools Protocol structure, details the frontend architecture, and provides step‑by‑step guidance for remote debugging, custom extensions, and session replay using WebSocket forwarding and server‑side recording.
Chrome DevTools is the most widely used tool for frontend development, capable of debugging regular pages, mobile webviews, mini‑programs, and even Node.js applications.
It offers a rich set of features such as DOM inspection, debugging, network monitoring, and performance analysis.
The article investigates why DevTools works across many scenarios, how it can be ported to new environments, and whether its capabilities can be modularized.
Chrome DevTools Composition
Chrome DevTools consists of four parts:
Debugger protocol: devtools-protocol based on JSON‑RPC 2.0.
Debugger backend: implements the protocol for entities like Chrome or Node.js.
Debugger frontend: the panel embedded in Chrome that communicates via the protocol; also includes tools such as Puppeteer and ndb .
Message channel: the communication layer (WebSocket, USB, ADB, etc.) essentially using socket communication.
The core of Chrome DevTools is the debugger protocol.
Chrome DevTools Protocol
The protocol is divided into domains, each exposing Methods, Events, and Types.
Methods correspond to request/response over sockets, Events to publish/subscribe, and Types to the data structures used.
# https://chromedevtools.github.io/devtools-protocol/1-3/Log
Log Domain
Provides access to log entries.
Methods
Log.clear
Log.disable
Log.enable
Log.startViolationsReport
Log.stopViolationsReport
Events
Log.entryAdded
Types
LogEntry
ViolationSettingA debugger backend must implement responses to Methods and emit Events when appropriate.
A debugger frontend uses Methods to request data and subscribes to Events.
browser_protocol & js_protocol
The protocol is split into browser_protocol (used by the browser backend) and js_protocol (used by the Node backend), each with its own TypeScript definitions.
js_protocol currently includes only four domains: Debugger, Profiler, Runtime (JS Runtime), and HeapProfiler, offering fewer capabilities than browser_protocol because web pages have a relatively fixed workflow while Node applications vary widely.
browser_protocol includes domains such as DOM, DOMDebugger, Emulation, Network, Page, Performance, and Profiler, covering all aspects of page development.
Chrome DevTools Frontend
The frontend, known as devtools-frontend, is the panel we normally use. Its source code resides in the ChromeDevTools/devtools-frontend repository.
Project Structure
After cloning the repository, the front_end directory contains a flat structure where each JSON file has a same‑named JavaScript file and optionally an HTML file. These represent applications such as inspector, node, devtools, and ndb.
# tree -L 1
.
├── accessibility
├── accessibility_test_runner
│ ├── AccessibilityPaneTestRunner.js
│ └── module.json
├── animation
├── application_test_runner
├── axe_core_test_runner
... (other directories)
├── input
├── inspector.html
├── inspector.js
├── inspector.json
├── network
├── network_test_runner
├── node_app.html
├── node_app.js
├── node_app.json
├── worker_app.html
├── worker_app.js
└── worker_app.jsonEach .json file defines an application; if it has a UI, an accompanying HTML file is present.
The devtools_app is the main debugging panel:
The inspector builds on devtools_app by adding page snapshots that update in real time and allow interaction.
Configuration File Semantics
// devtools_frontend/front_end/devtools_app.json
{
"modules": [
{"name": "emulation", "type": "autostart"},
{"name": "inspector_main", "type": "autostart"},
{"name": "mobile_throttling", "type": "autostart"},
...
{"name": "timeline"},
{"name": "timeline_model"},
{"name": "web_audio"},
{"name": "media"}
],
"extends": "shell",
"has_html": true
}modules : list of modules included in the application; each corresponds to a directory under front_end.
extends : indicates inheritance from another application (e.g., shell).
has_html : true when the application has an associated HTML UI.
Module Definition
{
"extensions": [
{
"type": "view",
"location": "drawer-view"
}
],
"dependencies": ["elements"],
"scripts": [],
"modules": ["animation.js","animation-legacy.js","AnimationUI.js"],
"resources": ["animationScreenshotPopover.css","animationTimeline.css"]
}extensions : custom attributes of the module.
dependencies : other modules this one depends on.
modules : JavaScript files belonging to the module.
resources : static assets such as CSS files.
The front_end directory uses its own module loading logic rather than standard Node or browser imports.
Initialization
Each application loads its JSON configuration, resolves dependencies, and then evaluates the required scripts.
// devtools-frontend/front_end/RuntimeInstantiator.js
export async function startApplication(appName) {
console.timeStamp('Root.Runtime.startApplication');
const allDescriptorsByName = {};
for (let i = 0; i < Root.allDescriptors.length; ++i) {
const d = Root.allDescriptors[i];
allDescriptorsByName[d['name']] = d;
}
if (!Root.applicationDescriptor) {
// load <appName>.json
let data = await RootModule.Runtime.loadResourcePromise(appName + '.json');
Root.applicationDescriptor = JSON.parse(data);
let descriptor = Root.applicationDescriptor;
while (descriptor.extends) {
// load parent config until none left
data = await RootModule.Runtime.loadResourcePromise(descriptor.extends + '.json');
descriptor = JSON.parse(data);
Root.applicationDescriptor.modules = descriptor.modules.concat(Root.applicationDescriptor.modules);
}
}
const configuration = Root.applicationDescriptor.modules;
const moduleJSONPromises = [];
const coreModuleNames = [];
for (let i = 0; i < configuration.length; ++i) {
const descriptor = configuration[i];
const name = descriptor['name'];
const moduleJSON = allDescriptorsByName[name];
if (moduleJSON) {
moduleJSONPromises.push(Promise.resolve(moduleJSON));
} else {
moduleJSONPromises.push(RootModule.Runtime.loadResourcePromise(name + '/module.json').then(JSON.parse.bind(JSON)));
}
}
// ... further loading logic
}Modules are instantiated, then the actual initialization occurs, establishing a socket connection to the debugger backend and preparing the UI using native DOM operations.
// devtools-frontend/front_end/main/MainImpl.js
new MainImpl(); // initialize SDK (protocol), socket, and communicationApplications
Remote Debugging
Using front_end, you can remotely debug a page: a developer watches page changes, network activity, and console output from another machine.
Opening a Debugging Port
Chrome’s embedded panel uses the Embedder channel, which cannot be used for remote debugging. Instead, a WebSocket channel is required, and Chrome must be started with the --remote-debugging-port flag.
Example command: [path]/chrome.exe --remote-debugging-port=9222 Or run the provided script devtools-frontend/scripts/hosted_mode/launch_chrome.js.
Chrome then starts an internal HTTP server exposing endpoints such as: /json/protocol – returns the supported protocol in JSON. /json/list – lists debuggable targets; each target’s webSocketDebuggerUrl is the address needed for remote debugging. /json/new, /json/activate/:id, /json/close/:id, /json/version – other management endpoints.
[
{
"description": "",
"devtoolsFrontendUrl": "/devtools/inspector.html?ws=localhost:9222/devtools/page/8ED9DABCE2A6BD36952657AEBAA0DE02",
"faviconUrl": "https://github.githubassets.com/favicon.ico",
"id": "8ED9DABCE2A6BD36952657AEBAA0DE02",
"title": "GitHub - Unitech/pm2: Node.js Production Process Manager with a built-in Load Balancer.",
"type": "page",
"url": "https://github.com/Unitech/pm2",
"webSocketDebuggerUrl": "ws://localhost:9222/devtools/page/8ED9DABCE2A6BD36952657AEBAA0DE02"
}
]Note: These interfaces cannot be accessed via cross‑origin AJAX; they must be called from a server or directly in the browser.
Connecting
After obtaining the webSocketDebuggerUrl, pass it as the ws query parameter to the frontend application.
// devtools-frontend/front_end/sdk/Connections.js
export function _createMainConnection(websocketConnectionLost) {
const wsParam = Root.Runtime.queryParam('ws');
const wssParam = Root.Runtime.queryParam('wss');
if (wsParam || wssParam) {
const ws = wsParam ? `ws://${wsParam}` : `wss://${wssParam}`;
return new WebSocketConnection(ws, websocketConnectionLost);
}
if (Host.InspectorFrontendHost.InspectorFrontendHostInstance.isHostedMode()) {
return new StubConnection();
}
return new MainConnection();
}Start a static server in the front_end directory (e.g., serve -p 8002) and open:
http://localhost:8002/inspector?ws=localhost:9222/devtools/page/8ED9DABCE2A6BD36952657AEBAA0DE02
The inspector UI will reflect all changes on the remote page.
Cross‑Domain Scenarios
If the frontend and backend are on different internal networks, a public server can act as a relay. Both sides connect to the public WebSocket server, which forwards messages between them.
Note: Because /json/list is an HTTP endpoint that cannot be called cross‑origin, the URL must be obtained manually and passed to the launcher script.
After retrieving the webSocketDebuggerUrl, replace the ws parameter in the frontend URL with the public server address.
The full relay consists of four nodes: the frontend, the public server, the launcher script running in the target page, and the debugger backend.
The server and launcher simply forward messages, enabling full interaction between the frontend and backend.
Warning: If the frontend sends Network.enable , the launcher page must not be used as the debugging UI because the forwarded Network.webSocketFrameReceived events can cause an infinite loop. Either block the Network.enable request or keep the launcher page purely as a data relay.
Server Code Example (WebSocket Relay)
// server.js
var WebSocketServer = require('websocket').server;
var http = require('http');
var server = http.createServer(function(request, response) {
response.writeHead(404);
response.end();
});
server.listen(3232, function() {
console.log(new Date() + ' Server is listening on port 3232');
});
wsServer = new WebSocketServer({ httpServer: server });
var frontendConnection;
var debugConnection;
wsServer.on('request', async function(request) {
var requestedProtocols = request.requestedProtocols;
if (requestedProtocols.indexOf('frontend') != -1) {
// handle frontend connection
frontendConnection = request.accept('frontend', request.origin);
frontendConnection.on('message', function(message) {
if (message.type === 'utf8') {
if (debugConnection) {
debugConnection.sendUTF(message.utf8Data);
} else {
frontendConnection.sendUTF(JSON.stringify({msg:'Debugger backend not ready, open the target page first'}));
}
}
});
frontendConnection.on('close', function(reasonCode, description) {
console.log('frontendConnection disconnected.');
});
}
if (requestedProtocols.indexOf('remote-debug') != -1) {
// handle target page connection
debugConnection = request.accept('remote-debug', request.origin);
debugConnection.on('message', function(message) {
if (message.type === 'utf8') {
var feed = JSON.parse(message.utf8Data);
if (feed.type == "remote_debug_page") {
// confirm connection
debugConnection.sendUTF(JSON.stringify({"type":"start_debug"}));
} else if (feed.type == "start_debug_ready") {
// target page ready
} else {
// forward to frontend
if (frontendConnection) {
frontendConnection.sendUTF(message.utf8Data);
} else {
console.log('Cannot forward to frontend, no connection');
}
}
}
});
debugConnection.on('close', function(reasonCode, description) {
console.log(new Date() + ' Peer remote ' + debugConnection.remoteAddress + ' disconnected.');
});
}
});Launcher Script (launched in the target page)
var host = "localhost:3232"
var ws = new WebSocket(`ws://${host}`, 'remote-debug');
var search = location.search.slice(1);
var urlParams = {};
search.split('&').forEach(s => {
var pair = s.split('=');
if (pair.length == 2) {
urlParams[pair[0]] = pair[1];
}
});
ws.onopen = function() {
ws.send(JSON.stringify({type:"remote_debug_page", url:location.href}));
};
ws.onmessage = function(evt) {
var feed = JSON.parse(evt.data);
if (feed.type == "start_debug") {
// connect to the real debugger WebSocket
var debugWS = new WebSocket(`ws://${urlParams.ws}`);
debugWS.onopen = function() {
ws.send(JSON.stringify({type:"start_debug_ready"}));
ws.onmessage = function(evt) { debugWS.send(evt.data); };
ws.onclose = function() { debugWS.close(); };
};
debugWS.onmessage = function(evt) { ws.send(evt.data); };
debugWS.onclose = function() {
ws.send(JSON.stringify({type:"remote_page_lost", url:location.href}));
};
}
};
ws.onclose = function() {
console.log("Connection closed...");
};Replay
When Page.enable and Network.enable are active, the debugger backend continuously pushes page snapshots and network data. By modifying server.js to timestamp and store these pushes, you can later replay the session.
if (message.type === 'utf8') {
var feed = JSON.parse(message.utf8Data);
if (feed.type == "remote_debug_page") {
debugConnection.sendUTF(JSON.stringify({"type":"start_debug"}));
} else if (feed.type == "start_debug_ready") {
writeStream = fs.createWriteStream(saveFilePath,{flags:'as',encoding:'utf8'});
} else {
if (frontendConnection) {
frontendConnection.sendUTF(message.utf8Data);
}
if (feed.method) writeStream.write(message.utf8Data+'
');
}
}Adding a feedback protocol allows the inspector to read the saved file and replay events with original timing.
if (requestedProtocols.indexOf("feedback") != -1) {
feedbackConnection = request.accept('feedback', request.origin);
feedbackConnection.on('message', function(message) {});
const fileStream = fs.createReadStream(saveFilePath);
const rl = readline.createInterface({input: fileStream, crlfDelay: Infinity});
for await (const line of rl) {
feedbackConnection.sendUTF(line);
rl.pause();
setTimeout(() => rl.resume(), 1000);
}
feedbackConnection.on('close', function(reasonCode, description) {
console.log('feedbackConnection disconnected.');
});
}With this setup you can record a debugging session and later replay it, supporting pause, seek, resume, and stop operations.
Using devtools‑frontend as a Library
The package chrome-devtools-frontend is available on npm, but importing it directly is inconvenient because the front_end applications rely on a custom module loader and expect all files to reside in the same directory hierarchy.
If you only need a single module, you can import it, but creating a new application is best done by copying the entire front_end directory and adapting it.
Chrome DevTools Extensions
Custom capabilities can be added to Chrome’s built‑in panel via Chrome extensions, e.g., vue-devtools .
References
ChromeDevTools/awesome-chrome-devtools
ChromeDevTools/devtools-protocol
WecTeam
WecTeam (维C团) is the front‑end technology team of JD.com’s Jingxi business unit, focusing on front‑end engineering, web performance optimization, mini‑program and app development, serverless, multi‑platform reuse, and visual building.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
