Compatibility

Does Obfuscation Break Tagged Templates?

A tagged template looks like syntax sugar and is actually a small contract with the language. The strings on either side of your substitutions are handed to the tag function as an array, and the specification requires that array to be the same object every time that particular piece of source is evaluated. An entire category of libraries is built on that guarantee, and none of them say so in their documentation.

Why the identity matters more than the text

When you write styled`color:${c};`, the tag function receives an array of the literal chunks and the interpolated values separately. The chunks never change for a given piece of source, so the language caches that array per call site and hands back the identical object on every evaluation. It is also frozen, along with its raw companion.

That identity is what the libraries memoise on. A css-in-js library uses it as a cache key so that rendering the same component a thousand times produces one class rule rather than a thousand. A template library uses it to decide that it has already parsed this template and can reuse the prepared result. A GraphQL tag uses it to avoid re-parsing a query document on every call.

The important consequence is that losing the identity does not produce an error. Every one of those libraries still works if the array is fresh each time -- it just stops hitting its cache. The symptom is a build that is correct and gradually slower, re-parsing and re-creating work it used to reuse, with memory growing where a cache keyed on object identity never gets a hit.

That is the kind of regression that does not appear in a test suite and does not raise an exception, which is why it is worth measuring deliberately rather than assuming.

What was measured

The sample checks the guarantee directly rather than through a library. It evaluates the same tagged template twice from the same function and asks whether the two arrays are the same object, using a Map keyed on the array exactly as a caching library would. It then evaluates a second, textually identical template from a different function to confirm that two distinct call sites do not collide into one entry.

It also records whether the strings array and its raw companion are frozen, what the cooked and raw forms of an escape sequence look like, that a tag with no substitutions still calls the tag, and that an untagged template is still an ordinary string.

In all five configurations the output was identical. The same call site returned the same object and the cache reported a hit; the second call site produced a separate entry; both the strings array and its raw array were frozen; the cooked form of a tab escape was a tab and the raw form kept the backslash; the zero-substitution tag was called with a one-element array; and the untagged template produced a plain concatenated string.

That is a complete pass on the parts of the contract that libraries depend on, in both output targets and with the string transforms enabled.

The breaks are in the member column, and both are loud

Tagged templates have a small surface that member renaming can reach, and it is the array the tag receives rather than the template itself.

Pointing member renaming at raw broke the sample immediately with TypeError: Cannot read properties of undefined. The raw property is created by the language on the strings array; your file asks for a generated name, finds nothing, and the first method call on it throws. Any tag that reads the raw form -- which includes anything doing its own escape handling -- fails at the first evaluation.

Pointing it at length produced a quieter result: the tag that reports strings.length read undefined instead of 1. Nothing threw, because reading an absent property is legal, and a tag that branches on how many chunks it received will take the wrong branch silently.

Both are the same rule this series keeps arriving at. The array handed to your tag is built by the language, so its property names are a language contract. Renaming is safe for names your own code both writes and reads, and unsafe for names something else created.

What this means for the libraries by name

For a css-in-js library, the thing to verify is that repeated renders of the same component do not produce a growing number of generated class names. That is the visible symptom of a lost cache, and it shows up in the document's style sheet rather than in the console.

For a template library, the check is that re-rendering the same template reuses its prepared result rather than re-preparing it. These libraries generally keep an internal cache keyed on the strings array, so the observable effect of losing identity is extra work per render rather than a wrong result.

For a GraphQL tag, the query document is parsed once per call site and cached. Losing the identity means re-parsing the document on every call, which is measurable in a hot path and invisible everywhere else.

In every one of these cases the base configuration measured correct, so this is a check to confirm rather than a problem to fix. The reason to run it is that the failure mode is a performance regression rather than an error, and performance regressions get attributed to whatever changed most recently.

A check worth running on your own build

Because the failure is silent by nature, it is worth having a two-line assertion rather than trusting a description. Define a tag that returns its strings argument, call it twice from the same function, and compare the two results with ===. If that comparison is true and Object.isFrozen on the result is also true, your build preserves the contract.

Run that assertion in the protected output rather than in your source, and run it in the configuration you actually ship, including whichever target version and string transforms your profile enables. The whole point is to check the emitted file.

If you maintain a library that depends on this, the same assertion is worth keeping as a test in your own suite. It costs nothing, it is stable across toolchains, and it catches a category of regression that no bundler reports.

If the assertion fails on an older protected build, the answer is to re-protect with a current release rather than to change your code: the per-call-site caching is emitted by the protection step, so it comes back with a rebuild.

Untagged templates, and what is not at risk

Untagged template literals are a different and much simpler case. They are string building, they lower to concatenation on the ES5 target, and there is no array and no identity to preserve. Nothing in this page applies to them.

The values you interpolate are ordinary expressions and are renamed like any other code, with no effect on the result. In the sample, the interpolated variable was renamed in every configuration and the output string was unchanged.

Escape handling is preserved in both directions, which matters for tags that do their own processing: the cooked array contained a real tab and the raw array contained a backslash followed by t, in every configuration. A tag that relies on the difference between the two forms gets the same input it did before protection.

The tag function itself is ordinary application code. It is renamed as an identifier, and that has no bearing on the contract, because the contract is about the array the language builds rather than the name of the function receiving it.

Frequently asked questions

Does obfuscation break tagged templates?

Not in anything measured here. The strings array was cached per call site and frozen, two textually identical templates from different call sites stayed separate, raw and cooked escape forms were preserved and a zero-substitution tag still called its tag, in all five configurations.

Why does the strings array identity matter?

Because css-in-js, template and GraphQL libraries use it as a cache key. If the array is fresh on every evaluation, those caches never hit, so the library re-parses or re-creates work it should reuse. Nothing throws, which is why the symptom is a gradual slowdown rather than a failure.

Will styled-components or lit-html still cache correctly in a protected build?

In the configurations measured here, yes: the same call site handed back the identical frozen array, which is the property those caches are keyed on. It is still worth confirming on your own build with a two-line assertion, because a lost cache shows up as extra work rather than as an error.

What breaks tagged templates in practice?

Member renaming reaching the strings array. A pattern matching raw makes the first read throw a TypeError, because raw is created by the language rather than by your code. A pattern matching length is quieter: a tag that branches on the number of chunks reads undefined and takes the wrong branch.

Do untagged template literals need any of this care?

No. An untagged template is string building with no array and no identity to preserve, and it lowers to concatenation on the ES5 target. Interpolated expressions are renamed like ordinary code and the resulting string is unchanged.

How do I check this on my own build?

Define a tag that returns its strings argument, call it twice from the same function and compare the results with a strict equality check, then confirm the result is frozen. Run it against the emitted file in the profile you ship, not against your source.

Related reading