Using measureUserAgentSpecificMemory with Cross-Origin Isolation

You want accurate, post-GC memory numbers from real users, but performance.measureUserAgentSpecificMemory is either missing or throws a SecurityError. This guide from Measuring Memory in Production Browsers, part of Browser DevTools & Performance Profiling Workflows, walks through enabling cross-origin isolation, calling the API correctly, and interpreting its breakdown without breaking the rest of your page.

Symptom Root Cause Immediate Action Measurable Impact
performance.measureUserAgentSpecificMemory is undefined Page not cross-origin isolated, or unsupported browser Check self.crossOriginIsolated; add COOP and COEP headers API becomes available in Chromium
API call rejects with SecurityError Isolation lost (e.g. headers missing on this response) Serve COOP/COEP on every document response, including errors Consistent availability across routes
Images or scripts fail after enabling COEP Cross-origin resources lack CORP/CORS Use COEP: credentialless or add crossorigin + CORS Isolation without broken assets
Popups and OAuth flows stop working COOP same-origin severs window.opener Use a redirect flow or isolate only pages that need it Login works while measured pages stay isolated
Promise takes 10–20 s to resolve API waits for a garbage collection Never await it on a critical path; run in the background No user-visible delay

Root Cause: Precise Memory Numbers Need Isolation

Memory measurement is a side channel. If a page could read precise heap sizes, it could load a cross-origin resource — an image, a JSON response, an iframe — and infer its size or even content by watching memory change. Browsers therefore gate precise measurement behind cross-origin isolation, the same mechanism that gates SharedArrayBuffer and high-resolution timers. A page is isolated when its document response carries two headers:

  • Cross-Origin-Opener-Policy: same-origin places the page in its own browsing context group, so cross-origin windows it opens (or that open it) cannot hold a reference to it.
  • Cross-Origin-Embedder-Policy: require-corp (or credentialless) guarantees that every cross-origin resource the page loads has explicitly consented, via Cross-Origin-Resource-Policy or CORS — or, with credentialless, is fetched without cookies so it cannot contain user-specific data.

With both in place, self.crossOriginIsolated is true, and performance.measureUserAgentSpecificMemory() becomes available in Chromium-based browsers. The call returns a promise that resolves after the browser’s next suitable garbage collection, which makes the numbers reflect live memory rather than whatever garbage happened to be present — a major improvement over performance.memory. The result includes the main frame, same-origin iframes and workers, with a per-context breakdown.

The hard part is not the API but the headers. require-corp blocks any cross-origin subresource that does not opt in: images from a CDN without CORP headers, third-party scripts loaded without crossorigin, embedded iframes whose content does not set COEP. credentialless is gentler — it allows no-CORS cross-origin requests but strips credentials — and is usually the best starting point. COOP same-origin also changes popup behaviour: pages opened across origins lose their window.opener link, which affects OAuth and payment popups.

From headers to the memory API The document response carries COOP same-origin and COEP credentialless or require-corp. COOP separates the page from cross-origin popups. COEP ensures every cross-origin subresource consents or is fetched without credentials. Together they set self.crossOriginIsolated to true, which unlocks measureUserAgentSpecificMemory, SharedArrayBuffer and precise timers. COOP: same-origin no cross-origin opener links COEP: credentialless subresources consent or go cookieless crossOriginIsolated === true measureUserAgent- SpecificMemory() SharedArrayBuffer precise timers both headers must be on the document response of every page you want to measure

Step-by-Step Fix

  1. Audit cross-origin subresources. In DevTools → Network, load your key pages and filter by domain to list every cross-origin image, script, font, iframe and fetch. Verification: you have a list of origins and whether each already sends CORS or Cross-Origin-Resource-Policy headers.
  2. Enable COEP in report-only mode first. Send Cross-Origin-Embedder-Policy-Report-Only: credentialless (and a Reporting-Endpoints header) to see what would break without breaking it. Verification: reports list any resource that would be blocked; the page still works.
  3. Fix or exempt breaking resources. Add crossorigin="anonymous" to scripts and images served with CORS, ask vendors for CORP headers, or proxy resources through your origin. Verification: the report-only endpoint receives no new violation reports for your key pages.
  4. Turn on enforcing headers. Serve Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: credentialless on document responses. Verification: in the Console, self.crossOriginIsolated returns true, and DevTools → Application → Frames → top shows the page as cross-origin isolated.
  5. Call the API in the background. Invoke performance.measureUserAgentSpecificMemory() from an idle, scheduled task and handle both success and SecurityError. Verification: the promise resolves (possibly after several seconds) with a bytes total and a breakdown array.
  6. Record a compact summary. Store total bytes, main-window JavaScript bytes and worker bytes with route and session age. Verification: your RUM backend receives samples tagged api: "uasm" that are consistent across repeated sessions on the same route.
A sample result, broken down by context Total 142 megabytes. The main window's JavaScript accounts for 88 megabytes, DOM memory attributed to the window for 21 megabytes, a dedicated data-processing worker for 26 megabytes, a same-origin help iframe for 5 megabytes, and unattributed memory for 2 megabytes. result.bytes = 142 MB; breakdown by attribution Window · JavaScript 88 MB Window · DOM 21 MB DedicatedWorker (parser) 26 MB iframe#help (same-origin) 5 MB unattributed 2 MB

Command and Code Reference

Use case: server headers for isolation, with a report-only phase. Shown for an Express app; the same headers can be set at a CDN or reverse proxy.

// server.js — isolate document responses only (not every asset)
app.use((req, res, next) => {
  if (req.accepts('html')) {
    res.set('Cross-Origin-Opener-Policy', 'same-origin');
    // Phase 1: observe breakage without enforcing
    // res.set('Cross-Origin-Embedder-Policy-Report-Only', 'credentialless; report-to="coep"');
    // Phase 2: enforce
    res.set('Cross-Origin-Embedder-Policy', 'credentialless');
    res.set('Reporting-Endpoints', 'coep="https://rum.example.com/coep", default="https://rum.example.com/reports"');
  }
  next();
});

Use case: call the API safely and summarise the breakdown.

// uasm.js — background measurement with a compact summary
export async function measureMemory() {
  if (!self.crossOriginIsolated || !performance.measureUserAgentSpecificMemory) return null;
  let result;
  try {
    result = await performance.measureUserAgentSpecificMemory(); // may take seconds
  } catch (err) {
    if (err.name === 'SecurityError') return null;               // isolation lost
    throw err;
  }
  let windowJs = 0, workers = 0;
  for (const entry of result.breakdown) {
    const scopes = entry.attribution.map((a) => a.scope);
    if (scopes.includes('Window') && entry.types.includes('JavaScript')) windowJs += entry.bytes;
    if (scopes.some((s) => s.endsWith('WorkerGlobalScope'))) workers += entry.bytes;
  }
  return { total: result.bytes, windowJs, workers };
}

// Run when the browser is idle, never on a critical path
requestIdleCallback(async () => {
  const summary = await measureMemory();
  if (summary) queueBeacon({ api: 'uasm', ...summary, route: location.pathname });
});

Verification and Regression Prevention

Isolation is verified when self.crossOriginIsolated is true on every page you intend to measure, your error logs show no increase in failed resource loads after rollout, and login and payment flows still work. The measurement is verified when repeated sessions on the same route produce similar totals at similar session ages, and when a deliberately leaking test build shows a rising curve in your dashboard.

Keep isolation from regressing by adding a smoke test that loads key pages and asserts crossOriginIsolated === true — a CDN or proxy change can silently strip the headers, and the API then quietly stops returning data. Monitor the share of samples with api: "uasm" versus api: "legacy"; a sudden drop in the modern share is usually lost isolation. Schedule measurements as described in sampling memory telemetry without hurting performance so the collector adds no measurable overhead.

Checking isolation on a measured page On each page you intend to measure, self.crossOriginIsolated must be true. If it is true and failed resource loads did not increase, measurement is safe. If it is false, the API is unavailable and a COOP or COEP header is missing. If it is true but failed loads rose, a cross-origin resource lacks CORP or CORS and is being blocked. self.crossOriginIsolated on this page Ready: measure and chart totals by session age true, no new load errors API unavailable: COOP or COEP header missing on this route false A cross-origin resource lacks CORP/CORS and is blocked true, failed loads rose

Edge Cases and Gotchas

Isolation applies per document

Every document response needs the headers, including error pages and pages served by a different backend. A single-page app that loads its shell once is simpler: isolate the shell response, and client-side navigations stay isolated.

Embedded iframes need their own headers

Under COEP, a cross-origin iframe must itself send COEP and CORP headers (or be credentialless-compatible), or it will be blocked. Third-party widgets that you cannot change may force you to leave those pages unisolated, or to use the credentialless iframe attribute where supported.

Service workers and caches

Responses served from a service worker cache must carry the isolation headers too. If you add COOP/COEP on the server but your service worker serves a cached shell without them, the page will not be isolated until the cache refreshes. Bump the cache version when enabling isolation.

Not a synchronous API

Because the result depends on a garbage collection, calling the API in response to a user action and awaiting it will feel broken. Treat it as asynchronous telemetry, and tolerate promises that resolve after the user has navigated away by sending results from a pagehide handler.

Frequently Asked Questions

Why is measureUserAgentSpecificMemory not available on my page?

The page is probably not cross-origin isolated, or you are testing in a browser that does not implement it. Check self.crossOriginIsolated in the Console; if it is false, the COOP and COEP headers are missing from the document response or are being overridden.

Should I use require-corp or credentialless?

Start with credentialless. It allows cross-origin no-CORS resources to load without credentials, which avoids most breakage. Use require-corp if you need credentialed cross-origin subresources and can make every one of them send CORP or CORS headers.

Will cross-origin isolation affect my analytics or ads?

It can. Third-party scripts loaded without CORS and iframes that do not send COEP-compatible headers may be blocked under require-corp, and some ad and analytics integrations rely on popups or opener access that COOP severs. Run COEP in report-only mode first, review the violation reports, and test ad and analytics flows explicitly before enforcing. If a critical integration cannot comply, keep the pages that need it unisolated and measure them with the legacy API.

How do I test isolation locally?

Serve your development build with the same headers — most dev servers accept custom headers in their configuration — and open the page from localhost, which counts as a secure context. self.crossOriginIsolated in the Console and the frame details in DevTools → Application → Frames confirm whether it worked.

Does the API force a garbage collection?

It does not force one immediately; it resolves after the next suitable collection, and implementations may schedule one if none happens within a time limit. That is why results can take many seconds and why they reflect live memory rather than garbage.