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.