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

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:

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:

<!doctype html> <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:

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