Compatibility
Published
Text handling above the basic multilingual plane is where correct-looking string code goes wrong, and it goes wrong in ways that only appear with real user input. Adding an obfuscation step to a codebase that handles names, messages or emoji raises a fair question. We measured the runtime behaviour across five configurations, and the answer separates cleanly into two parts: what protection does, and what your code was already doing.
Two different questions that get asked as one
There is a storage question and a behaviour question here, and they have different answers.
The storage question is what the protected file physically contains. That one is covered in does obfuscation break internationalization: the writer escapes characters above the ASCII range rather than emitting them directly, so a protected file holding Greek, Thai or emoji text contains no bytes above 127 at all. That is a representation change, and a mildly useful one, since an escape sequence has no character set for a proxy to get wrong.
The behaviour question is the one this page measures: once that file runs, does your string code do the same things? Does length return the same number, does slicing break in the same places, does sorting produce the same order, does normalization still compare equal? Those are runtime properties, and they are what actually breaks products.
We ran a twenty-two case sample through the ES5 target, the modern target, the two identifier-renaming presets and the string table. Every line matched the original.
What was measured, and what it shows
Length is reported in UTF-16 code units, and stays that way. A single grinning-face emoji has a length of 2 before and after protection. A woman-technologist emoji, which is a zero-width-joiner sequence of several code points, has a length of 5. Those numbers are unchanged by every configuration, including the string table that relocates and re-encodes the literal.
Code unit and code point access keep their distinct results. charCodeAt(0) returns the high surrogate 55357, charCodeAt(1) the low surrogate 56832, and codePointAt(0) the actual code point 128512. String.fromCodePoint(0x1F600) still reconstructs a value equal to the original literal, which is a direct check that the literal survived the table intact.
Iteration that is code-point aware stays code-point aware. Spreading or Array.from over a single emoji yields one element rather than two, and over the joiner sequence yields three. Slicing that is not code-point aware still is not: taking the first code unit of an emoji still produces a lone surrogate.
Normalization behaves identically. A combining sequence and its precomposed equivalent still compare unequal with === and still compare equal after normalize("NFC"). Case mapping still changes length where the language says it should: the German sharp s uppercases to two characters. And the regular expression u flag still makes the difference it should, with /^.$/ failing on an emoji and /^.$/u matching it.
The bugs in this area are yours, and they are preserved exactly
Several cases in the sample are deliberately wrong code, included because they are the failures people actually hit and then blame on the most recent change to the pipeline.
Reversing a string with split("").reverse().join("") operates on code units and tears surrogate pairs apart. Sorting an array of accented words with the default comparator orders them by code unit, which puts zebra before words beginning with an accented capital rather than where a reader expects. Both behaved identically before and after protection, on every configuration.
That is the useful result. Protection is faithful to the semantics of the original, including the parts of the original that are wrong. If emoji handling looks broken after you add an obfuscation step, the first thing to check is the same input against the unprotected build, because in our measurements the two agree.
The corollary is that fixing these is ordinary work unaffected by protection: iterate with Array.from or for...of when you need code points, compare with localeCompare when you need reader order, normalize before comparing text that came from different sources, and use the u flag on regular expressions that must match whole characters.
Why the transforms cannot reach this
The reason nothing moved is that none of the transforms operate on string semantics. Identifier renaming substitutes names within a scope. The string transforms change how a literal is stored and reassembled, not what it evaluates to.
The check that makes this concrete is string identity under the string table. After every literal has been moved into an encoded table and rebuilt at run time, "abc" === "ab" + "c" is still true, and a concatenation of a protected literal with an emoji still compares equal to the original combined value. Values are preserved, so every method operating on them behaves the same.
JSON is worth stating separately because it is a common boundary. Round-tripping an emoji through JSON.stringify and JSON.parse returned an equal value after protection, and encodeURIComponent produced the same percent-encoded bytes. Text that leaves your bundle leaves it unchanged.
What to do before shipping
Nothing specific to protection. If you already have tests covering names with accents, right-to-left text or emoji, running them against the protected bundle is sufficient evidence, and that is the general recipe in verifying the obfuscator did not break your code.
If you do not have those tests, the gap is worth closing on its own merits rather than because of obfuscation. A user with an emoji in their display name will find a code-unit assumption faster than any test suite will.
The one setting to review, as everywhere else in this series, is the member-renaming pattern. It is name-based and not type-aware, so a pattern broad enough to match a built-in method name rewrites those call sites and the method is no longer reachable. Keep it anchored and explicit, and scope it to names your own objects define.
Frequently asked questions
Does obfuscation break emoji in my JavaScript?
No. We measured a twenty-two case Unicode sample across five configurations covering both the ES5 and modern targets, two identifier-renaming presets and the string table, and every result was identical. That included string length in code units, surrogate pair access, code-point iteration and JSON round-tripping.
Does string length change after protection?
No. Length is reported in UTF-16 code units before and after. A single emoji still has a length of 2 and a zero-width-joiner sequence still has the same multi-unit length it had in the original. The string table changes how a literal is stored, not what it evaluates to.
My emoji handling broke after I obfuscated. What happened?
Test the same input against the unprotected build first, because in our measurements the two behave identically. The usual cause is code-unit logic that was already wrong: reversing with split on the empty string, slicing at a code-unit index, or sorting with the default comparator. Protection preserves those behaviours faithfully rather than introducing them.
Does normalize still work after obfuscation?
Yes. A combining sequence and its precomposed equivalent still compare unequal with === and still compare equal after normalize("NFC"), identically before and after protection on every configuration we ran.
Are non-ASCII characters stored differently in a protected file?
Yes, and this is a representation change rather than a behaviour change. The writer emits characters above the ASCII range as escape sequences, so the protected file is pure ASCII. The values are identical at run time, and a file with no bytes above 127 is immune to a mis-declared character set in transit.
Does the regular expression u flag still behave correctly?
Yes. Without the flag a dot matches a single code unit and fails to match a whole emoji; with the flag it matches the full code point. Both results were unchanged after protection, so regular expressions written to be code-point aware stay code-point aware.
Related reading