Compatibility
Published
Weak references are built on object identity, and object identity is the one thing a JavaScript obfuscator has no way to disturb. Renaming a variable does not create a new object; renaming a property does not copy anything. That makes WeakRef and FinalizationRegistry one of the safest surfaces in this series, with a boundary narrow enough to state in a sentence.
What was measured
The sample creates an object, wraps it in a WeakRef, and checks that deref() returns the identical object rather than a copy, using strict equality against the original. It then builds a FinalizationRegistry, registers the object with a held value and a separate unregister token, unregisters it and checks the return value, registers a second object with no token at all, and finally puts a WeakRef inside an ordinary cache object and looks the entry up through a helper.
That covers everything an application actually does with these APIs, short of waiting for a collection. Deliberately, the sample does not try to observe finalization: garbage collection is not scheduled by the specification, callbacks may never run, and a test that depends on them is a test that reports noise. What it does check is that the registry accepts every legal shape of registration and that the callback list is still empty when the program ends, which it should be while the target is alive.
Across five configurations -- the default target, the modern target, both gate profiles and the string-transform profile -- every line was identical. deref() returned an object strictly equal to the original, the identifier on it read back correctly, unregister returned true, the anonymous registration succeeded, the cache lookup returned the right value and incremented its hit counter.
The strict-equality result is the important one and it is worth saying why. Renaming is a consistent substitution over a program, not a transformation of the values that program creates. The object the WeakRef holds is the same object in memory whatever its variable is called, so deref() === target is not merely true after protection, it could not have been otherwise.
The one thing that breaks
The member column found exactly one failure and it is the expected one. A pattern matching deref, register and unregister threw a TypeError on the first call, on both targets. Those are the platform's method names on the platform's objects, and a protected file that asks a WeakRef for a generated method name is asking for something that does not exist.
Everything else in the member column measured identical, and the list of what was tested matters more here than usual. A pattern matching the fields of the referenced object came back clean. A pattern matching the cache wrapper's own names -- the property holding the WeakRef and its hit counter -- came back clean. So did a pattern matching the fields of the held value passed to register.
That last result is the interesting one, and it is a genuine difference from the other platform APIs in this series. When you register something, the second argument is a held value that the registry stores and hands back to your callback later. The registry never reads inside it. It is an opaque payload, so its field names are yours to rename as long as the callback that eventually reads them is renamed with it -- which, being in the same file, it is.
Compare that with the Intl options object, where the platform reads your dictionary by name and silently ignores what it does not recognise, or with a wasm import object, whose keys are fixed in a binary. The held value is the counter-example: a structure you hand to the platform that the platform genuinely treats as opaque.
Why the identity guarantees hold
It is worth separating the two guarantees these APIs make, because protection interacts with neither. The first is that a weak reference does not keep its target alive. That is a property of how the engine's collector treats the reference, decided by the runtime, and no source transformation participates in it.
The second is that deref() returns either the original object or undefined, never a substitute. That follows from the same fact that makes WeakMap and WeakSet safe under member renaming, which this site has measured separately: these collections key on identity rather than on a name, and identity is not something the transforms can express, let alone change.
The practical consequence is that the usual weak-reference bugs behave identically before and after protection. If your cache entry is collected earlier than you expected, that was true unprotected. If your finalization callback never runs, that was true unprotected. Protection does not make collection more or less likely, and it does not change when a callback is scheduled.
One caveat belongs here rather than in a footnote. Protection does change the shape of the code, and self-compression in particular introduces structures that live for the lifetime of the program. That does not affect the semantics of a weak reference, but if you are chasing a retention problem, the protected build is a different program to profile, and the article on profiling protected code covers how to read a heap snapshot of one.
Where this leaves a real cache
The realistic use of these APIs is a cache that holds results without pinning them in memory, and the realistic use of a registry is releasing an external resource when its wrapper is collected. Both were measured in miniature here and both survive.
The failure to design against is the method-name one, and it is easy to avoid because those three names are unusual. deref is not a name applications choose for their own properties. register and unregister are more plausible -- an event bus or a plugin system might well have them -- and that is the collision worth checking, because a member pattern written for your plugin registry will also rewrite the calls into a FinalizationRegistry.
That is the same collision the neighbouring articles keep finding with name, value, get and set. The names that cause trouble are never exotic; they are the ordinary ones that your code and the platform both wanted.
The general rule applies here with less force than almost anywhere else in this series, which is the honest summary: anchor the member pattern to your own names, and the only thing on this surface that can go wrong is a three-name collision that fails immediately and visibly on the first call.
How to check your own build
One assertion covers the identity guarantee: hold an object, deref() it, and assert strict equality with the original. If that passes, the reference is intact and no transform has substituted anything.
For a registry, assert that unregister returns true for a token you registered. That exercises the two method names most likely to collide with your own vocabulary and it does not depend on collection happening.
Do not write a test that waits for a finalization callback. It may never run, the specification does not promise it will, and a flaky test here will cost more time than the bug it is looking for. If you need to prove a resource is released, prove it with an explicit release path and treat finalization as the backstop it is meant to be.
Frequently asked questions
Does obfuscation break WeakRef?
No. Across five protection configurations a WeakRef dereferenced to an object strictly equal to the original, and the value read off it was unchanged. Renaming is a consistent substitution over a program rather than a transformation of the objects it creates, so the identity a weak reference depends on cannot be disturbed by it.
Does protection change when my objects are garbage collected?
Not in any way you can rely on or should design around. Collection is scheduled by the engine, and no source transformation participates in that decision. The usual weak-reference behaviours are unchanged: if an entry is collected sooner than you expected, or a finalization callback never runs, both were equally true before protection.
Can member renaming break a FinalizationRegistry?
Only through the method names. A pattern matching deref, register or unregister throws a TypeError on the first call, on both targets, because those names belong to the platform's objects. Everything else measured clean, including the fields of the referenced object and the wrapper holding the reference.
Is the held value I pass to register safe to rename?
Yes, and this is the one place a structure handed to the platform is genuinely opaque. The registry stores the held value and passes it back to your callback without ever reading inside it, so its field names are yours. Both sides of that read live in your own file and are renamed together, which is why it measured identical.
Which of these names is most likely to collide with my code?
register and unregister. An event bus or plugin system commonly uses both, and a member pattern written for yours will also rewrite the calls into a FinalizationRegistry. deref is rare enough in application code to be safe in practice. The failure is immediate and loud, so it will not reach production quietly.
Should I write a test that waits for finalization?
No. The specification does not promise a callback will ever run, so such a test is flaky by construction and will cost more than it finds. Assert strict equality through deref and assert that unregister returns true for a token you registered; both are deterministic and both cover the surface that protection can actually affect.
Related reading