Compatibility
Published
The question usually arrives during a migration. Someone needs shared memory back, or threads in a WebAssembly module, and that means turning on cross-origin isolation. The headers go on, half the embedded resources stop loading, the isolation check still reports false, and somewhere in the debugging the protected bundle becomes a suspect because it is the least readable thing in the build. It is almost always innocent, and the reason why is worth stating precisely — along with the two places where there genuinely is an interaction.
What isolation is decided by
Cross-origin isolation is a state the browser grants a document, and it is granted on the basis of response headers. The document needs an opener policy of same-origin, an embedder policy of require-corp or credentialless, and every resource it embeds has to satisfy that embedder policy — by carrying a resource policy header, or by being fetched with cross-origin sharing headers. When all of that holds, the page becomes isolated and the capabilities gated behind isolation become available: shared memory buffers, threads in WebAssembly, and finer resolution from the high-resolution timer.
Every input to that decision is a property of an HTTP response. None of them is a property of the contents of a JavaScript file. A transform that renames identifiers, restructures control flow and rewrites string literals changes what is inside a file; it does not change the status line, the content type, or any header your server attached to it. So the isolation state of a page is exactly the same before and after the protection step, and if the check reports false, the answer is in your header configuration or in something you embed. This is the most common version of the question and it has a short answer.
Delivery is where the two topics really meet
The one place isolation touches your bundle is in how the bundle is delivered rather than what it contains. Under require-corp, every subresource has to opt in, and a script loaded from another origin will be blocked unless that origin says it is willing to be embedded. If you serve protected assets from a CDN on a separate hostname, that CDN needs to send the appropriate resource policy or cross-origin sharing headers before an isolated page can load them.
This is true of unprotected bundles in exactly the same way, so it is not a cost of protection. It is worth calling out anyway, because cross-origin CDN delivery is a pattern that comes up here often in the context of integrity attributes, and enabling isolation adds a second header requirement to the same delivery path. If you are already computing integrity hashes for cross-origin scripts, the ordering rule is unchanged: hashes are computed over the final protected bytes, after every transformation, and the isolation headers are configuration on top of that.
The hidden frame, and the one failure worth knowing about
Here is the interaction that is genuinely non-obvious. Two of the runtime wrappers need a reference copy of the JavaScript built-ins that the page has not had an opportunity to modify, because comparing a possibly-tampered environment against itself does not tell you much. They obtain one the same way: by creating a hidden frame, reading what they need from its global, and removing it again. The self-healing wrapper takes a function constructor from it. The anti-tampering wrapper keeps the frame's global as a clean realm and compares the host page's built-ins against the pristine copies.
That frame is created without a source. It is a same-origin document that inherits the embedding page's origin and policy container, which means it inherits the embedder policy too. Nothing cross-origin is being fetched. So cross-origin isolation does not disturb it, and if you were expecting isolation to break these wrappers, it does not.
What does break it is a content security policy strict enough to forbid frames. A frame-src or child-src directive that excludes the frame stops it being created, and the way that failure is handled deserves attention: the anti-tampering wrapper catches it and carries on with its clean-realm references cleared, comparing the page's built-ins against themselves. Nothing is thrown and nothing is logged. The check keeps running with reduced power, and what it loses is exactly the class of modification it was there to notice. Teams tightening a policy and enabling these wrappers in the same quarter can end up shipping a weaker configuration than either change suggests, without any signal that it happened.
Timer precision, which sounds relevant and is not
Browsers coarsened the resolution of the high-resolution timer as a mitigation against timing side channels, and cross-origin isolation is what earns it back. Since one of the runtime options is a timing-based debugger check, the natural worry is that isolating your page makes that check more sensitive and therefore more prone to firing.
It does not, because the check does not use that counter. It reads the ordinary wall clock and compares the elapsed milliseconds between ticks against your configured interval plus five hundred. Wall-clock readings are unaffected by isolation, so the check behaves identically either way. This is a real "no interaction" answer rather than a hedged one, and it is useful to have because it removes a variable from the investigation. The check does have a serious accuracy problem, but its cause is browsers throttling timers in tabs the user is not looking at, which has nothing to do with isolation and is worth reading about separately.
Workers and WebAssembly under isolation
Two follow-on questions come up almost every time.
- Workers. A same-origin worker inherits the isolation state of the page that created it, which is what makes shared memory usable across them. Protected worker code is ordinary protected code; the transform has no special relationship with the worker boundary, and the existing guidance about what does and does not survive that boundary applies unchanged.
- WebAssembly modules. If threads are the reason you turned isolation on, you have probably moved hot code into a compiled module — and that module is outside the scope of the JavaScript transform entirely. The bundler integrations select emitted assets by file extension and default to JavaScript only, so a compiled binary is never a candidate for processing. Not skipped by a setting: never in the set. The glue code that instantiates it is protected normally, so the shape you end up with is a protected loader wrapped around an untouched binary.
That last point is the same boundary that applies to shader sources and to machine-learning model weights. Anything that is not JavaScript by the time the plugin runs is delivered as-is, and planning around it means deciding what is allowed to be readable rather than assuming the protection step reaches it.
A short checklist for the migration
- Confirm the isolation state directly in the page rather than inferring it from the headers you think you set.
- If it reports false, enumerate embedded resources and find the ones not satisfying the embedder policy. The protected bundle is only relevant here if it is served cross-origin.
- Give any separate asset origin the resource policy or sharing headers it needs, and keep integrity hashes computed over the final protected bytes.
- If you run a strict frame policy alongside the self-healing or anti-tampering wrappers, verify the clean realm is actually available, because losing it is silent.
- Treat compiled modules as delivered in the clear and decide accordingly what belongs inside them.
- Leave the debugger timing check out of the investigation; it reads the wall clock and is unaffected by isolation either way.
Frequently asked questions
Does protecting our bundle stop the page from becoming cross-origin isolated?
No. Isolation is granted by the browser on the basis of two response headers on the document, an opener policy of same-origin and an embedder policy of require-corp or credentialless, plus every embedded resource satisfying that embedder policy. All of those are properties of HTTP responses and of your server or CDN configuration. A code transform rewrites the contents of a JavaScript file; it does not emit headers, change a content type or alter how a response is served. If the isolation check on your page reports false, the cause is in your header configuration or in something you embed, and reverting the protection step will not change the answer.
Then what does actually interact?
Three things, and only one of them is likely to surprise you. First, delivery: under require-corp every subresource has to opt in, so a protected bundle served from a different origin needs the appropriate resource policy or cross-origin headers on that origin, exactly as an unprotected bundle would. Second, two of the runtime wrappers create a hidden frame in order to read untampered built-ins, which survives isolation but not every content security policy. Third, isolation restores finer timer resolution, which sounds relevant to the debugger timing check and turns out not to be, because that check reads the ordinary wall clock rather than the high-resolution counter.
Why do the runtime wrappers create a frame at all?
To get a reference copy of the built-ins that the page has not had a chance to modify. The self-healing wrapper takes a function constructor from the frame, and the anti-tampering wrapper uses the frame's global as a clean realm to compare the host page's built-ins against. It is created without a source, so it is a same-origin document that inherits the embedding page's origin and policy container. That inheritance is why cross-origin isolation does not disturb it: nothing cross-origin is being loaded, and the frame carries the same embedder policy as the document that created it.
What does break the hidden frame, then?
A content security policy that forbids frames. A frame-src or child-src directive tight enough to exclude the frame prevents it from being created, and this matters more than it sounds because of how the failure is handled. The anti-tampering wrapper catches the failure and continues with its clean-realm references set to null, falling back to comparing the page's built-ins against themselves. That is a quiet reduction in what the check can detect rather than an error you will see, and the thing it stops detecting is precisely the thing an attacker would have modified. If you run both a strict frame policy and these wrappers, check what you are actually getting.
We moved hot code into WebAssembly for threads. Is that protected?
Not by the JavaScript transform, and it is worth being clear that this is a scope boundary rather than a gap. The bundler integrations select emitted assets by extension and default to JavaScript files only, so a compiled WebAssembly module is never a candidate for the protection step. It is not skipped by a policy you can change; it never enters the set of files being processed. The JavaScript glue that instantiates the module is ordinary JavaScript and is protected normally, so what you end up with is a protected loader around an untouched binary. Plan for the module's contents to be readable by anyone who wants to read them.
Does isolation change how the debugger timing check behaves?
No, and the reason is worth knowing because the intuition points the other way. Cross-origin isolation restores finer resolution to the high-resolution performance counter, which browsers had coarsened as a spectre mitigation, so you would reasonably expect a timing-based check to become more sensitive. The debugger pause check does not use that counter. It reads the ordinary wall clock and compares elapsed milliseconds against your configured interval plus five hundred, so its behaviour is identical whether or not the page is isolated. Its real accuracy problem is unrelated to precision and has to do with browsers throttling timers in hidden tabs.
Related reading