Compatibility

Does Obfuscation Break data-* Attributes and dataset?

A data- attribute is a contract between two files that are protected by completely different rules: the HTML, which your build step never touches, and the JavaScript, which it rewrites thoroughly. That asymmetry is where the bugs live, and it is why this area deserves a measurement rather than an assumption.

What was measured

The sample models an element in JavaScript: a bag of attributes, a dataset object built from them by the same camelCase rule the platform uses, and getAttribute, setAttribute and matches on top. Modelling rather than calling a real DOM is deliberate here, and the limits of it are stated below.

It then does what widget code does. It reads data-role both ways, through getAttribute and through dataset.role. It reads a hyphenated attribute, data-user-id, through the camelCase property dataset.userId. It reads a missing key. It coerces data-open from the string "true" to a boolean, writes an attribute back, matches an attribute selector, and builds a configuration object out of the element the way an embeddable SDK reads its own host tag.

All five configurations produced identical output: the ES5 target, the modern target, the two identifier-renaming presets and the string-table preset that moves and encodes every literal. Attribute names are string data, the camelCase bridge is ordinary string manipulation, and neither is a rename site. The selector string [data-role=panel] came out intact even under the string-table preset, which relocates and encodes literals but preserves their values.

The failure mode, measured

Point member renaming at the keys your configuration object uses, ^(role|userId|open)$, and the page keeps running while every value it reads turns to nonsense.

dataset.role came back undefined, because the read was rewritten to a generated member while the property on the dataset object is still built from the attribute name in the markup. dataset.userId did the same, so Number(...) on it produced NaN. dataset.open came back undefined, so the comparison === "true" produced false where it had produced true.

Read that list again, because the shape of it is the point. A widget that was open is now closed, a user id is now NaN, and a role is now missing. Nothing throws. A boolean flipping to its default is the single hardest failure to notice in review, because a closed panel and a panel that was never asked to open look identical.

The equivalent identifier-renaming case is fine, which is what makes the member option the one to watch. Identifier renaming moves bindings, and both halves of a binding move together. Member renaming moves property names, and the other half of this particular contract lives in an HTML file it never sees.

What the model can and cannot tell you

Modelling the DOM in JavaScript measures the transform honestly, because the transform sees exactly the same code either way. What it cannot reproduce is that on a real element, half the contract is native.

On a real Element, dataset, getAttribute, setAttribute and matches are platform-owned names. In our model, renaming getAttribute renamed both the definition and the call, so the sample kept working. On a real element there is no definition to rename, so the call would simply be gone. The model therefore understates the risk on those names rather than overstating it, and the guidance is the same one that applies to every platform surface: keep the platform's names out of your pattern.

The dataset keys themselves are the interesting case, and there the model is exact. The property name is generated by the platform from the attribute name in your markup, so it is not a name your JavaScript owns even though it is spelled like one.

Why this is really a boundary question

This site keeps arriving at the same rule from different surfaces: a name is safe to rename when it is reached only by code the protector can see, and unsafe when it crosses a boundary the protector cannot follow. Usually the boundary is a network payload or a serialised object. Here it is markup, which feels less like a boundary because it ships in the same release.

The same reasoning covers three neighbours worth checking at the same time. Class names used by CSS are string data on both sides, so they survive. Element ids referenced from HTML are strings on both sides, so they survive. ARIA attributes and their values are strings, so they survive. A protected bundle keeps working with an unprotected stylesheet for exactly the reason a dataset key does not: nobody renames strings.

The awkward case is the embeddable widget, where the markup is written by your customer and the JavaScript is yours. If a member pattern reaches a configuration key, the failure appears only on customer pages, only for the options they happen to use, and never in your own test page if your test page uses different attributes. Configuration keys read off host markup deserve an explicit exclusion and a test that reads a realistic tag.

A check that fits in a release script

Build a fixture page containing every data- attribute your code reads, including the hyphenated ones, load the protected bundle against it, and print the configuration object your code derives. Compare it against the same print from the unprotected build.

If you want one grep instead, list the attribute names your code reads and check none of them matches your member pattern. The list is usually short and is worth keeping in the repository next to the pattern itself, since the two files change for different reasons and drift apart quietly.

Frequently asked questions

Does obfuscation change how data attributes are read?

No. We measured attribute reads through getAttribute, the dataset camelCase bridge including a hyphenated data-user-id, a missing key, boolean coercion, an attribute write, an attribute selector match and a derived configuration object, all identical across five configurations including the string-table preset.

Why does my widget read the wrong configuration after protection?

The most likely cause is a member renaming pattern that matches one of your dataset keys. We measured that case producing undefined for a role, NaN for a numeric id, and false for a boolean flag, with nothing thrown. The attribute in the markup is untouched, so the two halves of the contract no longer agree.

Are CSS class names and element ids affected by obfuscation?

No. Class names, ids, selector strings and ARIA attribute values are string data on both sides of the boundary, and strings are not rename sites. That is why a protected bundle keeps working against an unprotected stylesheet and unprotected markup.

Which names should an embeddable widget exclude from member renaming?

Every configuration key it reads off host markup, plus the platform names on Element such as dataset, getAttribute, setAttribute and matches. The widget case is the one where a mistake shows up only on customer pages, so keep the list of attribute names in the repository next to the member pattern.

Does the string-table option change my attribute names?

No. Moving and encoding literals relocates the text and preserves the value, so a selector string such as [data-role=panel] still compares equal at runtime. Our sample exercised exactly this and the selector match was unchanged.

What is the cheapest way to verify this on my own build?

A fixture page carrying every data attribute your code reads, loaded against the protected bundle, printing the configuration object your code derives from it. Diff that against the same print from an unprotected build; the hyphenated attributes are the ones worth including deliberately.

Related reading