Compatibility

Does obfuscation break cross-tab messaging?

Cross-tab coordination sits in a blind spot. It rarely appears in tests, it only misbehaves when a second tab is open, and when it fails the message is delivered and then ignored. Two of the three common APIs carry property names across the boundary and one does not, but the problem that actually bites in production is not naming at all.

Three APIs, one boundary

The APIs that let several tabs of the same application work together are BroadcastChannel for fan-out messaging, SharedWorker for a single worker shared by many pages, and the Web Locks API for mutual exclusion across contexts. They solve different problems but they share one property that matters here: each one puts your data in front of another execution context, and that context was not necessarily loaded at the same moment as the one sending.

That second half is the part people skip. A worker and its page load together. Two tabs do not.

BroadcastChannel carries property names

Everything posted through BroadcastChannel goes through the structured clone algorithm, and structured clone copies property names. Your message shape is therefore an interface, in exactly the way a message shape posted to a worker is an interface. The same reasoning applies, and the worker article works through that case in full.

Within a single build this is usually a non-event, and it is worth understanding why, because the reason tells you where the real risk is. Both ends of a BroadcastChannel are normally the same file: one module both posts and subscribes, and the two tabs are running two copies of it. Cross-file naming decisions inside a single protect run agree by construction, so the key the sender writes is the key the receiver reads. There is nothing to break.

The failure needs the two ends to have been protected separately. Two separate protect runs each pick their own names, and then one side writes a key the other side never looks for. That happens when a second entry point is protected in its own run, and it happens when the two tabs are not running the same build at all.

The version problem: two tabs, two builds

A browser tab can stay open for days. You deploy. The user opens a second tab, which fetches the new bundle, and now two tabs of your application are posting to the same channel while running code you built at different times.

This is where protection introduces something a plain build does not. By default the output is polymorphic per build: the configuration schema states it directly, that the same input, options and seed produce byte-identical output, and that omitting the seed gives per-build polymorphic output. That default is a deliberate and useful property, because it means two releases do not line up for anyone diffing them. It also means two builds of identical source do not agree on member names.

So if member renaming is on and your message payload is built from members, the old tab and the new tab disagree about the keys in every message they exchange. Both are correct programs. Neither logs anything. The messages arrive and do nothing.

There are three ways out and they are not equivalent. Pinning the seed makes the names line up across builds, which is the smallest change and is the right answer when the two builds are close together. Reserving the message keys is more durable, because a reserved name survives an options change that a pinned seed would not, and options do change between releases. Versioning the protocol and ignoring unknown versions is the most robust, and it carries a trap of its own: the version field is itself a member name, so it has to be reserved or the receiver cannot even read the version to decide it does not understand the message.

SharedWorker is a second entry point, and it outlives the page

A SharedWorker is a separate file with its own global scope, so the rule that governs dedicated workers governs it too: protect it in the same project and the same run as the code that talks to it, so cross-file naming decisions agree on both sides. Protecting it separately gives you two independent naming schemes for one shared message shape.

The sharper issue is lifetime. A shared worker can outlive every page that started it. A user closes the tab that spawned it, opens a new one, and the new page may connect to a worker that is still running the bundle you shipped last week. The version skew described above stops being an edge case and becomes the normal path immediately after a deploy.

If a shared worker holds state that matters, that lifetime is the thing to design around, protection or not. Protection only changes whether the mismatch shows up as a version number you can read or as a key that silently is not there.

Web Locks names are values, not members

The Web Locks API is the one that is structurally safe, and the reason is a useful illustration of the general rule.

When you call navigator.locks.request("sync-lock", callback), the lock name is a string argument to a function. It is not a member name and it is not a key in an object literal, so member renaming has no reason to touch it and does not.

String-table transforms do move the literal out of the call site and into a table that is read at runtime, so you will not find the text by searching the output. That is a change in where the value is stored, not a change in the value. What reaches the API is the same string that was in your source, which means two builds request the same lock and coordination continues to work across a deploy boundary.

This is a small point with a general lesson attached: when you are trying to predict whether a transform will break an API, the question to ask is whether the thing being transformed is a name or a value. Lock names, channel names and URLs are values. Payload keys are names.

How to test it in about ten minutes

Open two tabs and exercise the coordination path. That alone catches the separate-protect-run class of failure, and it is the test almost nobody runs.

Then simulate a deploy, which is the test that matters and takes barely longer. Build your application twice with different seeds, serve the first build to one tab and the second to another, and run the same coordination path. If the two tabs stop understanding each other, you have found the version-skew problem in development rather than in the hour after a release.

If you want the protected build you tested to be the protected build you shipped, pin the seed for that release. Byte-identical output from the same input and options is what makes a cross-tab test repeatable at all.

The short version

BroadcastChannel carries property names through structured clone, so message shapes are an interface, but within one build both ends agree by construction and nothing breaks. The real risk is two contexts running different builds, because per-build polymorphic naming is the default and two builds of the same source pick different names. A SharedWorker makes that worse by outliving the pages that started it. Web Locks are unaffected because a lock name is a value rather than a member. Reserve your message keys, pin the seed when two builds have to interoperate, and test with two tabs on two builds.

Frequently asked questions

Does BroadcastChannel break under obfuscation?

Within a single build both ends are normally the same file and their names agree by construction, so it works. The failures come from an end that was protected in a separate run, or from two contexts running different builds. The API is not the problem; the shared message shape is.

Why would two tabs be running different builds?

Because a tab can stay open across a deploy. The old tab keeps the bundle it loaded, a newly opened tab fetches the new one, and both keep posting to the same channel. A SharedWorker makes it sharper still, because the worker can outlive every page that started it.

Does pinning the seed fix cross-build message compatibility?

Pinning the seed makes two builds of the same input and options produce the same output, so their names agree. Omitting it gives per-build polymorphic output by design. Pinning is the smaller change, but reserving the message keys is more durable, because reserved names also survive an options change that a pinned seed would not.

Are Web Locks affected by member renaming?

A lock name is a string argument rather than a member name, so renaming leaves it alone. String-table transforms may move the literal into a table that is read at runtime, but the value handed to the API is the value from your source, so two builds request the same lock.

Do I need to protect a SharedWorker separately?

Protect it in the same project and run as the code that talks to it, exactly as you would a dedicated worker. A separate run picks its own names, and the two ends then disagree about the keys in every message they exchange.

What does a cross-tab naming failure actually look like?

A delivered message that is ignored. The listener fires, the payload arrives, the property the handler reads is not present, and the handler returns without doing anything. There is no exception and nothing in the console, which is why these are usually reported by a user rather than caught by a test.

Related reading