Compatibility
Published
Performance instrumentation has an awkward relationship with protection. It is the code you least want to change, because its whole job is to report faithfully on everything else, and it is written almost entirely in terms of names: mark names, measure names, entry types, and the fields of the entry objects the platform returns. Some of those names are strings, some are properties, and protection treats the two completely differently.
What was measured
The sample marks a start point, does a small amount of work, marks an end point, creates a measure spanning the two, and then reads the result back four ways: by name through getEntriesByName, by type through getEntriesByType, by inspecting the entry's name, entryType and duration fields, and by attaching a detail object to a mark and reading it out again.
That covers the two halves of the API cleanly. The mark and measure names are strings you choose. The entry objects are structures the platform builds and populates with fixed field names. A performance dashboard depends on both, and they behave differently under protection.
Across five configurations -- the default target, the modern target, both gate profiles and the string-transform profile -- the output was identical. One measure was found, its name was boot, its entryType was measure, its duration was a number, both marks were present with the names they were given, and the detail object round-tripped with its phase and step fields intact.
The string-transform result is the one worth dwelling on. MoveStrings and EncodeStrings relocate literals into a table and encode them, so the text boot-start does not appear anywhere in the protected file in a form you can grep for. It is still the value that reaches performance.mark at runtime, and the entry that comes back still carries it. Encoding a string changes where it is stored, not what it is.
Why the names survive and the fields do not
The distinction that explains every result here is one this site keeps returning to: a string is not a rename site. When you write performance.mark('boot-start'), the mark name is a value, and member renaming rewrites property names, not values. The mark keeps its name no matter how aggressive the member pattern is.
The entry object is the other case. When you write entries[0].duration, duration is a property name in your source, and a member pattern that matches it rewrites the read. The platform still puts a duration on the entry; your code asks for a generated name instead and gets undefined.
Measured with a pattern matching name, entryType and duration, that is exactly what happened: the measure was still created and still found, but the fields read undefined, and the list of mark names came back as an empty pair of commas because each entry's name read as nothing. The instrumentation ran perfectly and reported nothing.
The detail object splits the same way. A pattern matching detail, phase and step left the mark in place and readable, but the detail payload came back missing, because the property the platform copied it onto is called detail and the protected file was asking for something else.
The failure mode is a dashboard of blanks
It is worth being concrete about what this looks like in an application, because it does not look like a crash. A real user monitoring beacon usually walks the performance entries, pulls name and duration off each one, and posts a payload. With those two fields renamed, the walk succeeds, the array has the right length, and the payload posts cleanly. Every duration in it is undefined.
Downstream, that is a dashboard where the request count is right and every timing is null, or zero, or absent depending on how the collector coerces it. It is the same shape as renaming an error's message: the pipeline stays green and the content is gone. Monitoring code fails quietly by nature, because nothing is watching the watcher.
The method names, by contrast, fail immediately. A pattern matching mark, measure, getEntriesByName or getEntriesByType threw a TypeError on the first call. That is the good failure: it happens at startup, in development, on the first page load.
And the control arm was clean. A pattern matching only the names the sample owns -- its accumulator and its local list of marks -- measured identical on both targets, which confirms that nothing about the User Timing API is inherently fragile. The hazard is precisely and only the platform's field names.
What this means for instrumentation you ship
Performance instrumentation tends to sit in a small number of files, which makes it unusually easy to protect correctly. The names it depends on are a short, closed list: name, entryType, startTime, duration, detail, and the method names on performance itself. None of those are names an application would choose for its own domain objects by coincidence, with one important exception.
That exception is name, and it deserves attention because name is one of the most common property names in any codebase. A member pattern written to rename your own name fields -- on users, products, files -- will also rename the read of entry.name in your instrumentation, and the two have nothing to do with each other. This is the general argument for anchoring member patterns to specific, unusual names rather than writing a broad pattern and hoping.
There is also a measurement question that is separate from correctness. Protection changes how much code the engine parses and how it is shaped, so timings taken from a protected build are timings of the protected build. That is usually what you want, since it is what users run, but it means a before-and-after comparison of your own marks is measuring two different programs.
The transforms that add runtime work are the ones to keep in mind when reading those numbers. Self-compression adds a decompression step at load. Runtime defenses add periodic checks. Neither invalidates your marks; both are part of what you are now measuring.
How to check your own build
Add one assertion to whatever test already loads your application: create a measure, read it back with getEntriesByName, and assert that its duration is a number rather than undefined. That single check catches every renamed-field failure described here, because they all present the same way.
If you ship a monitoring beacon, the stronger check is to assert on the payload rather than on the entries. Serialise what you would have sent and confirm it contains real names and numeric durations. A beacon that posts successfully is not evidence that it posted anything useful.
For a build that already uses member renaming, grep your member pattern for name, value, type and duration before anything else. Those four are the intersection between field names the platform owns and names an application is likely to use for its own data, and they account for most of the surprises in this and the neighbouring articles.
Frequently asked questions
Does obfuscation break performance.mark and performance.measure?
No. Marks, measures, the entries returned for them and their durations came through five protection configurations unchanged, including both target versions and the string-transform profile. A mark name is a string argument, and member renaming rewrites property names rather than values, so the name a mark is created with is the name it keeps.
Do the string transforms change my mark names?
They change where the text is stored, not what it is. MoveStrings and EncodeStrings relocate the literal into a table and encode it, so you will not find boot-start by grepping the protected file, but the value that reaches performance.mark at runtime is identical and the entry that comes back carries it.
Why are all the durations in my monitoring dashboard undefined?
Most likely a member pattern matched the entry field names. The platform still populates name, entryType and duration on every entry; if your pattern renamed those reads, your code asks each entry for a property it does not have. The walk succeeds, the payload posts, and every value in it is undefined. Measured directly: the field reads returned undefined and the list of mark names came back empty.
Is PerformanceObserver affected in the same way?
It has the same two surfaces and the same rule applies to each. The entry types you subscribe to are strings and are safe. The options dictionary you pass and the fields you read off each delivered entry are property names, and a member pattern that reaches them will break the read rather than the subscription.
Which property name causes this most often?
name. It is both a field the platform owns on every performance entry and one of the most common property names in application code, so a member pattern written for your own name fields will also rewrite entry.name in your instrumentation. value, type and duration have the same collision problem and are worth checking in any member pattern.
Can I compare timings between a protected and an unprotected build?
You can, but be clear about what the comparison means: they are two different programs. Self-compression adds a decompression step at load and runtime defenses add periodic checks, so a protected build legitimately measures differently. For correctness testing, assert that a duration is a number; for performance work, measure the build you actually ship.
Related reading