Embedding the Perfetto UI
This guide shows you how to embed the Perfetto trace viewer inside your own
tool or dashboard via an <iframe> and feed it traces programmatically. This is
the right approach when you want the trace view to live within your app's
chrome, as real tools like Dart DevTools and various profiler frontends do. If
instead you just want to launch the full Perfetto UI in a new browser tab (the
window.open() flow), see Deep linking to the Perfetto UI;
that page also covers sharing URLs and appStateHash, which this guide does not
duplicate.
Before you begin
- Serve your host page over
http(s), notfile://. The embedding protocol relies onpostMessagebetween windows, which browsers disable forfile://origins. - Do NOT serve your host page with the
Cross-Origin-Opener-Policy: same-originheader. It breaks the parent/iframe relationship the UI depends on. - During local development, serve from
localhost/127.0.0.1. These origins are trusted by the UI, so traces open with no consent prompt (see Trust prompts and going to production).
Step 1: Add the iframe
Embed the UI with mode=embedded in the URL. This fully disables the sidebar
(not just hides it), which is what you want for an embedded view. The route is
hash-based:
<iframe
id="perfetto"
src="https://ui.perfetto.dev/#!/?mode=embedded"
width="100%"
height="600"
></iframe>In embedded mode the file-drop handler is also not installed, so the iframe only loads traces you post to it.
Step 2: Do the PING/PONG handshake
The postMessage channel into the iframe is not buffered: if you post a trace
before the UI has registered its message listener, the message is silently
dropped. To avoid this race, repeatedly post the string 'PING' until the UI
replies with 'PONG'. The UI only sends 'PONG' once its listener is
registered and document.readyState === 'complete'.
const iframe = document.getElementById('perfetto');
function waitForReady() {
return new Promise((resolve) => {
const interval = setInterval(() => {
iframe.contentWindow.postMessage('PING', '*');
}, 100);
window.addEventListener('message', function onMsg(evt) {
if (evt.source === iframe.contentWindow && evt.data === 'PONG') {
clearInterval(interval);
window.removeEventListener('message', onMsg);
resolve();
}
});
});
}Step 3: Post the trace
Once the handshake completes, post an object with a single perfetto key to the
iframe's contentWindow. Only buffer (an ArrayBuffer of raw trace bytes) and
title are required:
async function openTrace() {
await waitForReady();
const resp = await fetch(
'https://storage.googleapis.com/perfetto-misc/example_android_trace_15s',
);
const buffer = await resp.arrayBuffer();
iframe.contentWindow.postMessage(
{
perfetto: {
buffer: buffer,
title: 'My embedded trace',
},
},
'*',
);
}The full set of fields on the perfetto object:
buffer(required):ArrayBufferof raw trace bytes.title(required): string shown as the trace title.fileName(optional): suggested name if the user downloads the trace.url(optional): sharing URL. See Deep linking for howurlandappStateHashenable sharing.appStateHash(optional): 40-char hex hash restoring saved UI state.localOnly(optional): defaults totruefor posted traces, which disables download/share. Setfalseto re-enable them.keepApiOpen(optional): iftrue, the listener stays active so you can post more traces later. If omitted, the handler removes its listener after the first trace.pluginArgs(optional):{ [pluginId]: { [key]: unknown } }, passed to plugins'onTraceLoad().
NOTE: If you want to swap traces in the same iframe without reloading it, set
keepApiOpen: true on the first post. Otherwise the UI stops listening after
the first trace.
TIP: A bare ArrayBuffer is also accepted (the UI treats it as a trace titled
"External trace"), but posting the { perfetto: { buffer, title } } object is
preferred so you control the title.
Step 4 (optional): Drive the view
You can steer the embedded view in two ways.
To configure the UI as the trace opens, add startupCommands to the iframe
src as a URL-encoded JSON array of commands. For example, to pin the CPU
tracks:
const commands = [
{id: 'dev.perfetto.PinTracksByRegex', args: ['.*CPU [0-3].*']},
];
const src =
'https://ui.perfetto.dev/#!/?mode=embedded&startupCommands=' +
encodeURIComponent(JSON.stringify(commands));To scroll and zoom to a time range after the trace is loaded, post a second
message. timeStart and timeEnd are absolute trace time in seconds, not
relative to the trace start (most traces do not start at 0); a range outside the
trace is clamped to its bounds. viewPercentage is optional and is a fraction
in the range (0, 1] (e.g. 0.5 fills half the viewport, 1 fills it exactly);
out-of-range values are ignored and fall back to 0.5:
// e.g. zoom to the first 2 seconds of a trace that starts at 261187s.
iframe.contentWindow.postMessage(
{perfetto: {timeStart: 261187.0, timeEnd: 261189.0, viewPercentage: 1}},
'*',
);The UI retries this internally until the trace is ready, so you can post it shortly after the trace without your own wait loop.
Putting it together
Paste this into a file (e.g. index.html), serve it over http(s) from
localhost, and open it in a browser:
<html>
<body>
<iframe
id="perfetto"
src="https://ui.perfetto.dev/#!/?mode=embedded"
width="100%"
height="600"
></iframe>
<script>
const iframe = document.getElementById('perfetto');
const SAMPLE =
'https://storage.googleapis.com/perfetto-misc/example_android_trace_15s';
function waitForReady() {
return new Promise((resolve) => {
const interval = setInterval(() => {
iframe.contentWindow.postMessage('PING', '*');
}, 100);
window.addEventListener('message', function onMsg(evt) {
if (evt.source === iframe.contentWindow && evt.data === 'PONG') {
clearInterval(interval);
window.removeEventListener('message', onMsg);
resolve();
}
});
});
}
(async () => {
await waitForReady();
const buffer = await (await fetch(SAMPLE)).arrayBuffer();
iframe.contentWindow.postMessage(
{perfetto: {buffer, title: 'My embedded trace'}},
'*',
);
})();
</script>
</body>
</html>Trust prompts and going to production
The UI guards which origins may push traces:
localhost,127.0.0.1,[::1], same-origin, and a few hardcoded Google origins are trusted. Traces from these open immediately with no prompt, so local development just works.- From any other origin (e.g. your production domain), the UI shows a modal:
"<origin> is trying to open a trace file. Do you trust the origin?"
with No / Yes / Always trust. "Always trust" persists the origin in
localStorage, so each of your users sees the prompt at most once.
To avoid the consent modal entirely in production, self-host the Perfetto UI build on your own domain. A same-origin host page is trusted, so no prompt appears.
NOTE: ui.perfetto.dev follows the latest release, so the embedding protocol
described here is stable, though UI details may change over time. If you need a
fixed version, self-host the UI build to pin it. Self-hosting also gives the
same-origin trust benefit described above.
NOTE: The UI is client-only. Posted traces stay in browser memory and are never uploaded anywhere.
A complete example
The companion perfetto-embed
repository is a runnable end-to-end example: npm start serves a "devtool" host
page whose control panel embeds the UI and drives it (load traces, zoom, pin
tracks, run queries). It ships a small framework-agnostic PerfettoEmbed
wrapper you can copy into your own tool, plus a React variant.
See also
- Deep linking to the Perfetto UI:
the
window.open()(new tab) flow, plus sharing URLs andappStateHash. - Embedding API reference: the full list of messages and URL parameters the UI accepts.