components/surface_embed/guest_view_vs_surface_embed.md
GuestView and SurfaceEmbed are two different mechanisms in Chromium for
nesting or embedding web content (a child document/view) inside an outer
document (such as a WebUI page or Chrome App). This doc describes their differences.
GuestView (found in //components/guest_view) is the implementation
underpinning the <webview> tag in classic Chrome Apps, <controlledframe> in
isolated web apps (IWA), and MimeHandlerView. It reuses the Out-of-process
iframe (OOPIF) infrastructure.
The embedding relationship is recorded in the browser process in
content::FrameTree and WebContentsTreeNode.
Historically, it nested an inner content::WebContents inside an outer
content::WebContents. Modern GuestView implementations are migrating towards
a Multiple Page Architecture (MPArch) to avoid using inner WebContents and
instead structure the guest as a separate page within a GuestPageHolder.
SurfaceEmbed (found in //components/surface_embed) is a modern, lightweight,
and highly secure alternative. It embeds an independent content::WebContents
(e.g., a browser tab's content) inside an outer WebUI or document using a
blink::WebPlugin.
Rather than reproducing standard iframe frame/proxy structures, SurfaceEmbed
only bridges the visual layer by binding the inner WebContents's
RenderWidgetHostViewChildFrame directly to a compositor surface layer
(cc::SurfaceLayer) managed by the renderer-side plugin and the browser side
content::SurfaceEmbedConnector. It does not provide a scripting API between
the parent and the child document.
| Feature | GuestView (<webview>) | SurfaceEmbed (<embed>) |
|---|---|---|
| DOM Element | <webview> custom HTML element | <embed> with custom MIME type |
| Browser-side Container | WebContents (traditional) or GuestPageHolder (MPArch) | WebContents |
| Compositing Attachment | RemoteFrame + CrossProcessFrameConnector (reusing OOPIF) | blink::WebPlugin + SurfaceEmbedConnector |
| Ownership | Outer frame owns the guest (Tightly coupled) | Decoupled (User owns child WebContents) |
Scripting APIs (e.g., postMessage) | Fully supported (uses iframe-like remote frame proxies) | None |
| Frontend API | Extensive (navigation, script injection, event listeners) | None |
| Navigation Control | JS-driven (via src attribute) | Browser-driven (via WebContents API) |
In GuestView, the guest context is tightly coupled to the outer WebContents
that embeds it.
WebContents owns the inner WebContents once
attached.GuestPageHolder within the
parent's FrameTree / Page context. Therefore, the guest page is owned and
destroyed directly along with the parent frame/page hierarchy. Its lifetime
cannot be easily separated or reparented.WebContents for <webview> and then attach it when needed.Under SurfaceEmbed, the lifetimes of the outer frame and child WebContents
are completely decoupled:
WebContents is typically spawned and owned by the browser-side
client (e.g., the browser's tab strip or side panel coordinator), NOT the outer
WebContents.SurfaceEmbedConnector, the connection to child is established via a
raw_ptr<content::WebContents>. It does not govern the inner page's lifecycle.content::SurfaceEmbedConnector::Attach and detachment is done via
content::SurfaceEmbedConnector::Detach. This allows the inner WebContents to
be detached, moved, and re-attached to a completely different browser
window/WebUI without destroying the page or losing its state. This enables
preloading and deferred attachment.WebContents (attached via
AttachAsInnerWebContents()).WebContents instance. Instead, it is represented as a
separate Page hosted in a GuestPageHolder inside the outer WebContents's
FrameTree. This abstracts WebContents operations away from the guest and
restricts direct WebContents level API access (such as attaching arbitrary
tab helpers).GuestPageHolder work is currently behind a disabled-by-default
feature flag.SurfaceEmbed retains the use of a full, independent content::WebContents
instance (the child WebContents) for the embedded child.WebContents instance, the browser can
attach arbitrary TabHelpers (e.g., find-in-page, autofill, permission
managers, print preview, downloads) and interact directly with its navigation
and lifecycle APIs.GuestView, SurfaceEmbed does not route
input/layout/compositor frames through a nested frame tree or
RenderFrameProxyHost. Instead, content::SurfaceEmbedConnector coordinates
the mapping. When the child creates its RenderWidgetHostViewChildFrame, the
connector sends the FrameSinkId to the renderer's SurfaceEmbedWebPlugin,
which embeds the surface using cc::SurfaceLayer.Because <webview> / GuestView embeds the guest frame using cross-process frame
routing primitives (RenderFrameProxyHost in the browser, blink::RemoteFrame
in the renderer), it inherits the standard HTML frame hierarchy connectivity.
contentWindow.postMessage to communicate with the
child.SurfaceEmbed intentionally excludes all scripting and standard
cross-frame communication APIs:
contentWindow exposure and no JavaScript representation of
the child page in the parent DOM window tree.postMessage or frame relation properties (like
window.parent or window.opener) do not work and are completely blocked.cc::SurfaceLayer.<webview>'s API vs. <embed>'s API<webview>: Initial load and navigation are done by setting the src
attribute in JavaScript.<embed>: Loading and navigation are done by calling the WebContents
API on the embedded WebContents in the browser process.The <webview> tag exposes a rich, comprehensive JavaScript API for scripting,
navigation, and state inspection:
const webview = document.querySelector('webview');
webview.src = "https://example.com";
webview.addEventListener('contentload', () => { ... });
webview.executeScript({ code: "alert('Hello')" });
webview.insertCSS({ code: "body { background: red; }" });
webview.goBack();
These APIs allow the webpage to control navigation, intercept network requests, customize context menus, inject scripts, and listen to detailed page load/error states.
The <embed> tag used by SurfaceEmbed does not expose any programmatic
API to the embedding page's JavaScript:
<embed type="application/x-chromium-surface-embed" data-content-id="[content-id]">
content-id is generated by
GuestContentsHandle::CreateForWebContents(web_contents).id().<embed> element is treated by JavaScript as a standard web plugin
(similar to a PDF reader)..src, methods like .goBack(), or events
like contentload.WebUIController (using Mojo or chrome.send), and the browser-side code
directly invokes the child WebContents's NavigationController.This is a non-exhaustive list of differences.
window.open(url, target):
<webview>: Will navigate the embedded page, even if the target is
_blank or _top.<embed>: Can customize the behavior by overriding
WebContentsDelegate on the embedded WebContents (not possible with
<webview>).<webview>: The position of the <webview> in the parent document
affects the visibility results for elements inside the guest.<embed>: The positioning in the parent document does not affect
the results inside the guest, as it is treated as an independent visual
surface.<webview>: Cannot be captured (via navigator.mediaDevices) if the
parent document is invisible (e.g., in an inactive tab).<embed>: Can always be captured regardless of parent visibility.