Files
Dunqing 34ff7b4ee8 feat(minifier): drop write-only property assignments to unused local bindings by default (#24112)
## What

Drop `o.x = value` when `o` is a local variable that is never read — by default. Until now this only happened with the `treeshake.property_write_side_effects: false` opt-in. terser does this by default; esbuild does not do it at all.

This matters because one leftover write keeps a lot of code alive. antd bundles 820 icon modules that each end with `o.displayName = 'UserOutlined'`; that single write kept each module's whole require-chain alive. **antd.js: 2.22 MB -> 2.13 MB minified** (458 kB -> 455 kB gzip). oxc now beats esbuild on antd by ~180 kB.

## How

The drop itself uses machinery that already exists for the opt-in path (`is_member_assign_to_unused_binding`: the object must be a fresh value — function/class/object/array literal — not exported, never written as a whole, and every read of it must be the target of a property write).

What is new is making it safe without the opt-in:

- A program-wide fact table (`PersistentSymbolFacts`, bit `SymbolFact::MEMBER_WRITE_HAZARD`) is filled once by `Normalize`, before the compress loop. The container is generic on purpose: monotone symbol facts, seeded pre-loop, never cleared — a stale set bit only forgoes an optimization. A follow-up migrates `proto_write_symbols` into it as `PROTO_WRITTEN`, fixing that field's execution-order TODO. A symbol goes in when any of its property writes could be observed: read-modify ops (`o.x += 1` reads the property), chained writes (`a.b.c = 1` reads `a.b`, so dropping `a.b = {}` would throw), and `__proto__` or computed keys (may install setters). A hazarded symbol never gets its writes dropped.
- The set also grows mid-loop when a pass rewrites `o.x = o.x + 1` into `o.x += 1`.
- Only plain `=` with a single-level, non-`__proto__` key qualifies. `o[k()] = v` bails.
- A side-effectful right side is kept: `o.x = impure()` becomes `impure()`.

Once the write is gone, the variable is unused and the existing passes delete the declaration and everything only it referenced.

## Behavior change

The only assumption this makes (documented in `docs/ASSUMPTIONS.md`): setters installed on `Object.prototype` / `Function.prototype` / `Array.prototype` are not observed by these writes — the same assumption the existing opt-in flag makes, and terser's default.

A 98-case audit (node ground truth vs oxc vs terser, strict and sloppy) hardened everything else. Writes that throw or have exotic effects are now kept even though the binding is unused, via a kind-aware key denylist: `caller`/`arguments`/`name`/`length` on function- and class-valued bindings, `prototype` on class-valued (class prototypes are non-writable; plain function `prototype` writes still drop — they are writable and a real size win), `length` on array-valued (the array length setter coerces its RHS and can throw RangeError), private-field writes (brand-check TypeError), and classes with static blocks, decorators, or static accessors are not treated as fresh. Post-audit the matrix shows zero unsound cases beyond the documented prototype-setter class — stricter than terser, which for example still drops `a.length = -1` (losing the RangeError) where oxc now keeps it. No size cost: the minsize corpus is byte-identical, antd keeps the full 90 kB win.

## Example

```js
// input
(function() {
  var r = require('react');
  var i = require('./asn/UserOutlined');
  var o = function(e, t) { return r.createElement(i.a, e, t); };
  o.displayName = 'UserOutlined';
})();
```

```js
// before (everything kept)
(function(){var e=require("react"),t=require("./asn/UserOutlined"),n=function(n,r){return e.createElement(t.a,n,r)};n.displayName=`UserOutlined`})();

// after (only the side effects remain)
(function(){require("react"),require("./asn/UserOutlined")})();
```

Found by re-minifying oxc's minsize output with `terser -c`; this was the single largest gap between oxc and terser. Full conformance suite passes with zero snapshot changes; minsize stays a fixed point (antd takes one extra compress iteration).

Implemented with Claude Code, reviewed and verified by me.
2026-07-08 02:28:23 +00:00
..