Compatibility
Published
Almost every non-trivial front end has an event layer somewhere: a bus, an emitter, a pub/sub helper, or a framework's own. It is a nervous area for obfuscation because it mixes two things the transforms treat completely differently. Event names are strings. Method names are code. We measured a full emitter implementation to see what that difference actually costs.
The two halves of an event system
An emitter is worth measuring precisely because it sits on the line the member-renaming transform draws. When you write bus.emit("user:login", name), the engine sees two entirely different kinds of thing in one line.
emit is a member name. It is part of the program's structure, it is a rename site, and if member renaming is enabled it can be rewritten. "user:login" is a string literal. It is data. It may be moved into an encoded table and reassembled at runtime, but its value is preserved exactly.
That asymmetry is the whole story of this article, and it is the same mechanism that decides the outcome for translation catalogues and JSON boundaries. What makes the emitter case interesting is that the two halves meet in every single call.
What was measured
The sample is a complete prototype-based emitter with on, off, once and emit, backed by a handler registry keyed by event name. It covers handler ordering, the return value of emit, removal by function identity, a once handler that unsubscribes itself during dispatch, a handler that removes a different handler mid-dispatch, argument forwarding, method chaining, the value of this inside a handler, and event names assembled at runtime from two variables.
Across all five configurations — the ES5 target, the modern target, the two identifier-renaming presets and the string table — the output matched the original exactly. Handlers fired in registration order, once fired exactly once, removing a handler during dispatch did not corrupt the loop, and emitting an unregistered event returned zero rather than throwing.
The registry keys are the detail worth calling out. After protection, Object.keys(bus.handlers) still returns user:login. The event name is intact and still human-readable in the protected bundle, because it is a string, and strings keep their values.
Renaming the emitter's own methods is safe
We then ran the sample again with member renaming pointed at the emitter's own API: a pattern matching on, off, once, emit and the internal handlers registry. The output was still identical, on both targets.
That result is the expected one, and the reason matters. Within a single file the engine renames the declaration and every call site together. If emit becomes a generated name, the places that call it become the same generated name, and the program is internally consistent. Renaming is a whole-program substitution, not a mangling of one side.
We also ran it with the pattern scoped to the internal handlers property alone, leaving the public method names untouched. Also identical. Hiding the internal registry name while keeping the public surface stable works exactly as you would hope.
The boundary where this stops being safe
Everything above holds because the emitter and its callers were protected together, as one program. The failure mode appears when they are not.
If your bus is defined in one bundle and consumed by another that was protected separately, or not protected at all, then the two builds do not share a rename map. One side calls emit and the other defines a generated name. Nothing warns you at build time; the call fails at run time with a message naming a property that does not appear in your source. The same applies to a plugin API, a third-party integration that registers handlers on your bus, or anything crossing a micro-frontend boundary.
The second boundary is reflection. If you look a handler up by building its name as a string — bus["on" + suffix], or a dispatch table mapping strings to method names — the string is not a rename site and will not be rewritten to match the renamed member. This is the general rule that also governs JSON boundaries: renaming is safe when a name is only reached by code you control and hazardous when it is part of a contract with something outside the build.
Both cases have the same remedy. Member renaming is opt-in and is meant to be scoped: keep any name that crosses a boundary out of the pattern, and prefer marking genuinely internal members rather than turning the rule loose on every property. The Protect Members documentation covers how the pattern is applied.
What this means for your event layer
For a self-contained application bundle, an event emitter needs no special handling. It measured clean on every configuration, including with its own API renamed, and the dispatch-order edge cases that are genuinely tricky to implement stayed correct.
Two expectations are worth adjusting rather than worrying about. Your event names remain readable in the protected output, so if you regarded a name like internal:license-check-failed as private, it is not; that is the same honest boundary described in what obfuscation does not rename. And the shape of your event traffic stays observable to anyone instrumenting the bus at run time, because protection is a static transform and the bus is a live object.
The single checklist item is the boundary question: does anything outside this build call into the emitter by name? If yes, keep those names out of the rename pattern. If no, you are done.
Frequently asked questions
Does obfuscation break event listeners?
Not for code protected as a single program. We measured a full emitter with on, off, once and emit across five configurations covering both targets, the identifier-renaming presets and the string table, and every result was identical, including handler ordering, removal by identity and once semantics.
Are my event names visible in the protected bundle?
Yes. Event names are string literals, and string literals keep their values through every transform. Moving them into an encoded table changes how they are stored, not what they are, and they are reassembled at run time. If you treated an event name as a secret, treat it as public instead.
Can I rename the emitter's own methods with member renaming?
Yes, when the emitter and its callers are protected together. We ran the sample with a pattern matching on, off, once, emit and the internal handler registry, and the output was identical on both targets, because the engine renames declarations and call sites together within one program.
What breaks if my event bus is shared between bundles?
The rename map is per build, so two separately protected bundles do not agree on generated names. One side will call a method the other no longer defines, and it fails at run time rather than at build time. Keep any member name that crosses a bundle boundary out of the member-renaming pattern.
Does looking up a handler by a string name still work?
The string is preserved but it is not a rename site, so it will not be rewritten to match a renamed member. If you access methods reflectively by building their names as strings, exclude those names from the rename pattern. Event names used as registry keys are unaffected, because both sides of that lookup are strings.
Do handlers still fire in registration order after protection?
Yes. Registration order, the count returned by emit, once firing exactly once, and the case where a handler unsubscribes another handler during dispatch all produced identical output after protection on every configuration we ran.
Related reading