Last updated 2026-02-25.
This document explains how Site Isolation affects the architecture of Web Inspector in WebKit, describes the design changes made to support cross-process inspection, and outlines the work remaining. For a primer on Site Isolation itself — RemoteFrames, BrowsingContextGroups, and provisional navigation — see Site Isolation.
Web Inspector's backend is organized as a collection of agents, each responsible for one
protocol domain (Network, Page, DOM, Debugger, etc.). Historically, all agents for a
given inspected page lived in a single WebCore::Page in a single WebContent Process. A single
InspectorBackend handled all commands; InspectorBackendDispatcher routed each JSON-RPC
command to the correct agent.
PageInspectorController owns the agents and the BackendDispatcher for a Page. Commands
from the frontend arrive as JSON strings, get parsed in UIProcess, and are dispatched to the
correct PageInspectorController via the target routing system.
This design works perfectly when all frames share one process — but breaks down under Site
Isolation, where a WebPageProxy may have its frames distributed across several WebContent
Processes, each with its own Page and PageInspectorController.
To persist a debugging session across WebProcess swaps (introduced with PSON), the concept of inspector targets was introduced. A target is an opaque handle that:
- Provides a stable
targetIdthe frontend can route commands to across process swaps. - Allows the same protocol interfaces to be reused across execution context types (Page, Worker, Frame).
- Lets the frontend reason about the capabilities of each backend independently.
WebPageInspectorController in UIProcess manages the set of active targets. The Target
domain in InspectorTargetAgent exposes target lifecycle events (Target.targetCreated,
Target.targetDestroyed) to the frontend, and routes incoming commands to the correct target's
BackendDispatcher.
Before Site Isolation work, there were three target types:
Page— legacy direct-backend target (pre-PSON and WebKitLegacy). No sub-targets.WebPage— represents aWebPageProxy. May have transient worker and frame sub-targets.Worker— represents a dedicated Web Worker spawned from a Page.
Site Isolation adds a fourth:
Frame— represents an individualWebFrameProxy/LocalFrame, each potentially in its own WebContent Process.
When Site Isolation is off, the architecture is essentially unchanged from the pre-SI model:
- One
WebPageInspectorTargetProxy(typeWebPage) is created for theWebPageProxy. - All agents live in one
PageInspectorControllerin one WebContent Process. didCreateFrameonWebPageInspectorControlleris a no-op — no frame targets are created.- Commands are routed through the page target to
PageInspectorController.
When Site Isolation is enabled, each WebFrameProxy gets its own inspector target:
- One
WebPageInspectorTargetProxystill exists for page-level agents. - Each
WebFrameProxycreation triggers aWebFrameInspectorTargetProxy(typeFrame). - Each frame target connects to a
FrameInspectorControllerin the owning WebContent Process. - Commands targeted at a frame ID are routed to the correct
WebFrameInspectorTargetProxy, which sends them over IPC to theFrameInspectorControllerin that process.
The key callsite is in WebFrameProxy's constructor
(UIProcess/WebFrameProxy.cpp):
page.inspectorController().createWebFrameInspectorTarget(
*this, WebFrameInspectorTarget::toTargetID(frameID));And in the destructor, the target is torn down symmetrically:
page->inspectorController().destroyInspectorTarget(
WebFrameInspectorTarget::toTargetID(frameID()));This means frame targets are always present when frames exist, regardless of whether a frontend is connected — consistent with how page and worker targets behave.
UIProcess
┌─────────────────────────────────────────────────────────┐
│ WebPageInspectorController │
│ ├── WebPageInspectorTargetProxy (type: WebPage) │
│ │ └── PageInspectorController (in WCP-A) │
│ ├── WebFrameInspectorTargetProxy frame-1 (main) │
│ │ └── FrameInspectorController (in WCP-A) │
│ └── WebFrameInspectorTargetProxy frame-2 (cross-origin)
│ └── FrameInspectorController (in WCP-B) │
└─────────────────────────────────────────────────────────┘
IPC ↕ IPC ↕
WebContent Process A WebContent Process B
PageInspectorController (no PageInspectorController)
FrameInspectorController FrameInspectorController
InspectorTargetAgent (in JavaScriptCore/inspector/agents/InspectorTargetAgent.cpp) is the
glue layer. It receives all incoming commands from the frontend, looks up the target by targetId,
and calls sendMessageToTarget() on the appropriate InspectorTargetProxy.
For frame targets, WebFrameInspectorTargetProxy::sendMessageToTarget() sends the message over
IPC to WebFrameInspectorTarget in the WebContent Process, which calls
FrameInspectorController::dispatchMessageFromFrontend().
FrameInspectorController owns agents for a single frame. Not every domain has been moved to
per-frame agents yet — only Console is fully per-frame today. For unimplemented domains,
commands must fall through to the page-level PageInspectorController.
This is accomplished by passing the parent BackendDispatcher as a fallback when
constructing the frame-level one (FrameInspectorController.cpp):
FrameInspectorController::FrameInspectorController(
LocalFrame& frame, PageInspectorController& parentPageController)
: m_backendDispatcher(BackendDispatcher::create(
m_frontendRouter.copyRef(),
&parentPageController.backendDispatcher())) // <-- fallbackWhen BackendDispatcher::dispatch() receives a command for a domain not registered in
the frame-level dispatcher, it forwards the call to its fallback dispatcher — the page-level
BackendDispatcher. This makes per-domain migration incremental: a domain can be moved from
PageInspectorController to FrameInspectorController independently, and the fallback chain
ensures correct routing at every intermediate state.
InstrumentingAgents uses the same fallback pattern: a frame's InstrumentingAgents holds a
pointer to the parent page's InstrumentingAgents. When instrumentation fires in the frame
process (e.g., a network event), it first notifies frame-level agents and then falls through to
page-level agents for any domain not yet migrated.
Command from frontend
│
▼
FrameInspectorController.backendDispatcher
│
│ domain registered at frame level?
├── yes ──► frame-level agent handles it
│
└── no ───► fallback to PageInspectorController.backendDispatcher
│
▼
page-level agent handles it
WebFrameProxy is created in UIProcess whenever a new frame is established (both same-process
and cross-process frames). Its constructor calls createWebFrameInspectorTarget(), which calls
addTarget() in WebPageInspectorController. If a frontend is connected, this fires
Target.targetCreated to notify the frontend immediately.
When a frontend connects and enumerates targets, WebFrameInspectorTargetProxy::connect()
sends an IPC message to the WebContent Process hosting the frame. On the WebProcess side,
WebFrameInspectorTarget::connect() (WebProcess/Inspector/WebFrameInspectorTarget.cpp)
creates a WebFrameInspectorTargetFrontendChannel and connects it to FrameInspectorController:
void WebFrameInspectorTarget::connect(
Inspector::FrontendChannel::ConnectionType connectionType)
{
if (m_channel)
return;
Ref frame = m_frame.get();
m_channel = makeUnique<WebFrameInspectorTargetFrontendChannel>(
frame, identifier(), connectionType);
if (RefPtr coreFrame = frame->coreLocalFrame())
coreFrame->protectedInspectorController()->connectFrontend(*m_channel);
}When a frame-level agent emits an event (e.g., Console.messageAdded),
WebFrameInspectorTargetFrontendChannel::sendMessageToFrontend() sends it over IPC to UIProcess
(WebProcess/Inspector/WebFrameInspectorTargetFrontendChannel.cpp):
void WebFrameInspectorTargetFrontendChannel::sendMessageToFrontend(
const String& message)
{
if (RefPtr page = protectedFrame()->page())
page->send(Messages::WebPageProxy::SendMessageToInspectorFrontend(
m_targetId, message));
}UIProcess receives it in WebPageInspectorController::sendMessageToInspectorFrontend(), which
calls InspectorTargetAgent::sendMessageFromTargetToFrontend() to deliver the event — tagged
with the frame's targetId — to the frontend.
During provisional navigation, a frame may briefly exist in two processes simultaneously (see
Provisional Navigation). The inspector mirrors this:
WebFrameProxy is created for the provisional frame in the same constructor path, so it gets an
inspector target immediately. If the provisional load commits, the old frame target is destroyed
and the new one persists. If the load fails, the provisional frame target is destroyed with no
observable change to the frontend.
WebFrameProxy's destructor calls destroyInspectorTarget(). WebPageInspectorController
removes the target and fires Target.targetDestroyed to the frontend.
Console is the first domain fully migrated to per-frame agents. Each FrameInspectorController
owns a FrameConsoleAgent (see the constructor in FrameInspectorController.cpp). Console
messages originating from cross-origin iframes now appear in Web Inspector correctly attributed
to the originating frame, rather than being lost or mis-attributed.
Network and Page domains remain as Page Target agents — they do not become per-frame agents
and there is no BackendDispatcher fallback involved. Instead, the design splits each domain
agent across two processes:
- UIProcess side —
ProxyingNetworkAgent/ProxyingPageAgentlive in UIProcess as part ofWebPageInspectorController. They handle all command dispatch and own the authoritative view of network and page state. - WebContent Process side — A
NetworkAgentProxyin each WebContent Process hooks intoInstrumentingAgentsto capture per-frame network events (resource loads, responses, etc.) and forwards them over IPC to the UIProcess agent.
This means command routing for Network and Page never traverses the FrameInspectorController
fallback chain. All Network/Page commands arrive at the UIProcess agent directly via the Page
target, and the UIProcess agent is responsible for fanning out to the appropriate WebContent
Process when per-frame data is needed (e.g., Network.getResponseBody).
Page domain adaptation mirrors Network. Page.getResourceTree must collect and merge frame
subtrees from each WebContent Process. The merged result presents the frontend with a unified
frame tree even though resources are distributed across processes.
Phases:
- Phase 1 —
getResourceTreeaggregation across frame targets (in progress) - Phase 2 —
searchInResourcesacross all frame targets - Phase 3 —
getResourceContentwith correct process routing - Phase 4 — Resource load events aggregated from all processes
Web Inspector must continue to work with backends shipping in iOS 13 and later, which have no Frame targets. The frontend's target iteration logic handles this:
- If a
WebPagetarget has one or moreFramesub-targets → send per-frame commands to the frame targets. - If a
WebPagetarget has noFramesub-targets (older backend) → treat the page target as the single frame and send all commands there.
No frontend code needs to know whether it is talking to a single-process backend or a Site-Isolated backend — the frame target abstraction provides uniform addressing.
-
getResponseBodyrouting — Response body data lives inNetworkResourcesDatain the process that loaded the resource. When a frontend requests a body for a cross-origin iframe resource, how does the proxy agent locate and fetch it from the correct process? Current thinking: embed process identity in the resource identifier, or introduce a UIProcess-side cache. -
Shared
InjectedScriptManager—FrameInspectorControllercurrently shares the parent page'sInjectedScriptManager. Is this correct? Injected scripts run in a specific frame's JS context; a shared manager may cause leakage of script handles across origins. -
DOM domain across process boundaries — DOM
nodeIdvalues are process-local integers. Under Site Isolation, nodes from different processes may have colliding IDs. A global identifier scheme (possibly an extension ofInspectorIdentifierRegistry) is needed before DOM can be migrated to per-frame agents.
| File | Role |
|---|---|
UIProcess/Inspector/WebPageInspectorController.h/.cpp |
Manages all targets for a WebPageProxy |
UIProcess/Inspector/WebFrameInspectorTargetProxy.h/.cpp |
Frame target proxy in UIProcess |
UIProcess/Inspector/WebPageInspectorTargetProxy.h/.cpp |
Page target proxy in UIProcess |
UIProcess/Inspector/InspectorTargetProxy.h |
Base class for all target proxies |
UIProcess/WebFrameProxy.cpp |
Creates/destroys frame inspector targets on frame lifecycle |
WebProcess/Inspector/WebFrameInspectorTarget.h/.cpp |
Frame target in WebContent Process |
WebProcess/Inspector/WebFrameInspectorTargetFrontendChannel.cpp |
IPC: WebProcess → UIProcess for events |
WebCore/inspector/FrameInspectorController.h/.cpp |
Per-frame agent controller with fallback chain (frame-targeted domains) |
WebCore/inspector/PageInspectorController.h/.cpp |
Per-page agent controller (legacy + fallback target) |
WebCore/inspector/InstrumentingAgents.h |
Agent registry with fallback to parent controller |
WebKit/UIProcess/Inspector/ProxyingNetworkAgent.h/.cpp |
Network agent in UIProcess; receives events from per-WP NetworkAgentProxy |
WebKit/UIProcess/Inspector/ProxyingPageAgent.h/.cpp |
Page agent in UIProcess; handles getResourceTree aggregation |
JavaScriptCore/inspector/agents/InspectorTargetAgent.cpp |
Target multiplexing and command routing |
JavaScriptCore/inspector/InspectorBackendDispatcher.cpp |
BackendDispatcher with fallback dispatcher |