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-originplaces 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(orcredentialless) guarantees that every cross-origin resource the page loads has explicitly consented, viaCross-Origin-Resource-Policyor CORS — or, withcredentialless, 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.
Step-by-Step Fix
- 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-Policyheaders. - Enable COEP in report-only mode first. Send
Cross-Origin-Embedder-Policy-Report-Only: credentialless(and aReporting-Endpointsheader) to see what would break without breaking it. Verification: reports list any resource that would be blocked; the page still works. - 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. - Turn on enforcing headers. Serve
Cross-Origin-Opener-Policy: same-originandCross-Origin-Embedder-Policy: credentiallesson document responses. Verification: in the Console,self.crossOriginIsolatedreturnstrue, and DevTools → Application → Frames → top shows the page as cross-origin isolated. - Call the API in the background. Invoke
performance.measureUserAgentSpecificMemory()from an idle, scheduled task and handle both success andSecurityError. Verification: the promise resolves (possibly after several seconds) with abytestotal and abreakdownarray. - 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.
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.
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.
Related
- Measuring Memory in Production Browsers — the parent topic
- Replacing performance.memory in Real User Monitoring — migrating from the legacy API
- Sharing Memory Between Worker Threads with SharedArrayBuffer — another capability unlocked by isolation, in its Node.js form
- Browser DevTools & Performance Profiling Workflows — the section overview