Compatibility
Published
The question usually arrives from a team that supports something awkward: an enterprise fleet pinned to an old browser, a kiosk running whatever shipped with the device, a market where low-end Android is the norm. They want to know whether adding a protection step will quietly cost them the bottom of their matrix. The answer is no, and the reason is worth understanding precisely, because it also tells you where in your pipeline the step belongs.
A protector is not a transpiler
These two kinds of tool are easy to confuse because both rewrite JavaScript and both sit in a build. They do opposite things to your language level.
A transpiler exists to change it. You tell it which environments you support, and it converts syntax those environments cannot parse into syntax they can, adding helper functions where a construct has no direct equivalent. Changing your support floor is the entire product.
A protector exists to change how readable a program is while leaving its behaviour and its language level alone. It parses your source into a representation of the program, rewrites names and structure, and writes JavaScript back out at the level it read. An arrow function goes in and an arrow function comes out. A class stays a class. Nothing is lowered to an older dialect, and nothing is raised to a newer one.
Which means your support matrix is settled entirely by your transpiler and its target configuration, before the protector ever runs.
The missing setting is the evidence
If you go looking through the configuration for an ECMAScript version or output target, you will not find one. There is an optimisation mode, but its values select a runtime profile — automatic, general web, HTML5, game, mobile, Node — rather than a language level. It decides which runtime behaviours suit the environment, not which syntax the file is written in.
The absence is not an oversight. A tool that rewrote your syntax level would be obliged to expose that choice, because getting it wrong is catastrophic and silent. There is no such option here because there is no such transformation.
The corollary is that the parser has to be able to read whatever you actually wrote, and it does: classes, block-scoped declarations, template literals including tagged forms, arrow functions, async and await, dynamic import, import metadata, spread, and decorators on classes and their members. That capability is what allows the protection step to sit at the end of your pipeline instead of forcing you to protect a downlevelled bundle.
What about the code the engine adds?
This is the more interesting half of the question, and the one people rarely think to ask. Preserving your syntax level is only useful if the engine does not then inject helper code written in something newer. A runtime guard emitted as an arrow function would raise your floor no matter how carefully your own source was targeted.
The injected guards are written conservatively. They use classic function expressions and var declarations rather than arrow functions or block-scoped bindings, and they lean on long-standing array helpers rather than recent additions. Where a guard needs the global object it reaches for the modern name behind a typeof check and falls back to a function-based lookup when that name is missing.
So enabling a domain lock, a date lock or an integrity check does not smuggle newer syntax into a bundle you had carefully targeted at an older engine.
A small free win on unreliable clients
One behaviour genuinely helps compatibility rather than merely not hurting it. The writer escapes characters above code point 255 into four-digit unicode escapes, and the string encoding option additionally respells characters below 256 as two-digit hex escapes.
The practical consequence is that protected output tends to contain no bytes outside the ASCII range, even for an application shipping text in scripts that need them. A file with that property cannot be corrupted by a server or proxy that sends a mistaken character set header — a failure mode that is rare on a well-configured modern stack and distinctly less rare on the old, proxied and appliance-mediated networks where wide support matrices tend to matter. The same behaviour is covered from the translation angle in does obfuscation break internationalization.
Order, which is where this can actually go wrong
Everything above holds when the protection step runs last. Transpile, bundle, then protect, and treat the protected artifact as final.
Reversing that is where teams create the problem they were worried about. If your transpiler runs after the protector, it reads and rewrites protected output. That is slow on a file deliberately made structurally awkward, it can undo work the protector just did, and it makes any resulting failure hard to attribute because two tools have now rewritten the same code. It also breaks the more general rule that every tool which works by reading code — linters, scanners, extractors, coverage instrumentation — belongs before the step designed to make code hard to read. The ordering argument in minify before or after obfuscation is the same argument in different clothing.
The failures that do happen
When a protected build misbehaves on one browser and not another, syntax is rarely the cause. Two other things are.
The first is a runtime lock rejecting a client. A guard configured for browsers classifies the user agent into a category, and a client it cannot classify lands in a category that will not be in your allowlist. That is a deliberate fail-closed design and it is the correct behaviour for the threat it addresses, but it means an unusual browser, an embedded webview or a device with a customised user agent can be refused for reasons that have nothing to do with parsing. The same shape applies to an operating-system lock.
The second is simply that old devices are slow. A heavier transform adds startup work, and on a low-end phone that can read as a hang to a user and as a compatibility bug to a developer. It is a budget question rather than a support question, and it is answered by measuring on the actual device.
How to check it in an afternoon
Take the oldest and least capable configuration you claim to support, and run your existing test suite against the protected artifact rather than against source. A syntax problem announces itself immediately and unambiguously as a parse error on load, so this is a fast, high-signal check.
Add two things to that pass. Do a cold start on a genuinely low-end device and look at the time to first interaction, so a performance cost does not get logged as a compatibility bug. And if you use a browser or operating-system lock, exercise it on the unusual clients in your matrix specifically, because that is the one option capable of refusing a browser your code would otherwise have run in perfectly well.
The short version
The protector preserves the language level it reads, which is why there is no target setting to configure. The guards it injects stay within conservative syntax, so they do not raise your floor either. Escaped output is a minor bonus on unreliable networks. Your support matrix belongs to your transpiler, and the way to keep it intact is to protect last and then test the protected artifact on real hardware from the bottom of the matrix.
Frequently asked questions
Does obfuscation change which browsers my code runs in?
In the normal arrangement it does not move your support floor at all. The engine is a source-to-source transform: it parses your JavaScript, rewrites it, and emits JavaScript at the same language level it read. An arrow function goes in and an arrow function comes out. A class stays a class. Nothing is downlevelled to an older form and nothing is upgraded to a newer one. Your support matrix is decided by whatever transpiler and target configuration you already run, and the protection step leaves that decision exactly where it was.
Is there a target or ECMAScript version setting I should be configuring?
There is no language-level target option, and its absence is informative rather than a gap. A tool that transpiled would need one. Because this one does not, the only related setting is an optimisation mode that selects a runtime profile, with values covering automatic selection, general web, HTML5, game, mobile and Node. That chooses which runtime behaviours make sense for the environment, not which syntax the output is written in. If you go looking for a setting that turns the output into an older dialect, you will not find one because that transformation is a different tool's job.
Does the engine understand modern syntax in my source?
Yes. The parser handles classes, block-scoped declarations, template literals including tagged forms, arrow functions, async and await, dynamic import, import metadata, spread, and decorators on classes and class members. That matters because a parser that failed on modern syntax would force you to protect a downlevelled bundle, which is a worse arrangement for reasons covered below. Being able to read what you actually wrote is what lets the protection step sit where it belongs.
Do the injected runtime guards raise my minimum browser version?
They are written conservatively enough that in practice they do not. The emitted guard code uses classic function expressions and var declarations rather than arrow functions or block-scoped bindings, and the array helpers it relies on are long-standing ones. Where it needs the global object it reaches for the modern name behind a typeof check and falls back to a function-based lookup when the name is absent. So a domain, date, browser or operating-system guard does not quietly introduce syntax your oldest supported browser cannot parse.
What order should protection and transpilation run in?
Transpile first, protect second, and treat the protected artifact as final. This puts the protection step at the end of the chain where its output is what ships. Running it the other way means your transpiler reads and rewrites protected output, which is slow, which can undo structural work the protector just did, and which makes any failure much harder to attribute. The same reasoning governs the relationship with minification, and the general principle that every tool which works by reading code should run before the step designed to make code hard to read.
Does protected output work in browsers that predate the syntax I wrote?
Only if your transpiler already made that true, which is the whole point of keeping the order right. The protector preserves your language level, so if your source uses syntax an old browser cannot parse, the protected output uses it too and fails in the same place for the same reason. Protection neither rescues nor damages that situation. Establish your support floor with your transpiler and its target configuration, verify it there, and then protect the result.
How do character encoding and charset headers interact with this?
Favourably, and it is one of the few places the protector genuinely helps compatibility. The writer escapes characters above code point 255 as four-digit unicode escapes, and the string encoding option additionally respells characters below 256 as two-digit hex escapes. Protected output therefore tends to contain no bytes above the ASCII range even when your application ships text in scripts that need them. A file like that cannot be corrupted by a server or proxy sending a mistaken character set header, which is a real and irritating class of failure on older and less predictable clients.
Do polyfills still work after protection?
Yes, because a polyfill is ordinary JavaScript and is treated as ordinary JavaScript. The one thing to check is ordering rather than transformation: a polyfill bundle has to execute before the code that depends on it, and that requirement is unchanged. If your polyfills and your application are separate files, protect them the same way and keep the load order. If they are bundled together, the bundle is one program and the relative order inside it is preserved.
Does feature detection survive?
It does. Feature detection is a runtime test against the environment, usually a typeof check or a property lookup on a global. Those are ordinary expressions, and rewriting a program does not change what the environment reports back. Where teams occasionally get into trouble is with detection code that inspects function source text or relies on the name of a function, and both of those are fragile for reasons that predate protection and are worth removing regardless.
What actually goes wrong across browsers, if not syntax?
Almost always something about the environment rather than the language. A guard configured for browsers that classifies an unrecognised user agent into a category not in your allowlist will refuse to run on a client you did not anticipate. Older engines are also slower, so a heavier transform on a low-end device shows up as a startup delay that is easy to misread as a compatibility failure. Neither is a parsing problem, and both are found the same way, which is by running the protected artifact on a real device from your support matrix.
How should we test the support matrix after enabling protection?
Run the tests you already have against the protected build rather than against source, on the oldest and least capable configuration you claim to support. A parse error announces itself immediately and unmistakably, which makes this a fast check. The subtler things worth including are a cold start on a low-end device to see the startup cost, and one pass with any runtime guard enabled, because a lock configured for browsers or operating systems is the option most likely to reject a client for reasons unrelated to syntax.
What is the short version for a team with a wide support matrix?
Your matrix is your transpiler's responsibility and the protector does not participate in it. Keep the order as transpile, then bundle, then protect. Do not go hunting for a language-level setting, because none exists and none is needed. Expect the injected guards to stay within conservative syntax. Take the ASCII-only output as a small free win on unreliable clients. Then verify on real hardware from the bottom of your matrix rather than reasoning about it.
Related reading