Oxc can shorten variable names and private class names, but it cannot shorten property names such as `obj._field`. This change adds optional property mangling. It replaces #23772 and gives bundlers a way to use the same property names across several files. ### Feature - `include` selects names to mangle. `exclude` and `reserved` keep names unchanged. - The cache can give a property a fixed output name or keep it unchanged. - The mangler supports member access, object and class keys, destructuring, optional chains, the left side of `in`, and JSX property names. - `mangle_quoted` also handles strings and template literals used directly as property keys. `/* @__KEY__ */` and `/* #__KEY__ */` can mark one other string or template literal as a property key. - Short names are assigned in a stable order. More common properties get shorter names. Existing and reserved names are avoided. - Bundlers can collect names from several programs, create one shared map, and apply it to every program. - Numeric keys, `__proto__`, `constructor`, `prototype`, TypeScript-only keys, and private names are never changed by this pass. ### Design Property names are part of JavaScript syntax, not local scope. For this reason, the code lives in `oxc_minifier`. It only reuses the short-name generator from `oxc_mangler`. For one program, the order is: ```text collect names → assign names → rewrite once → compress → mangle variables and private names ``` Property names are rewritten before compression because compression can remove information about quotes and comments. For several programs, a bundler collects names from each program, merges the results, assigns one shared map, and rewrites each program once. Collection can run in parallel. All programs must use the same options. The shared map keeps property names consistent inside one build. The cache has a different job: it keeps fixed names and reservations across separate minify calls. ### Example With `include: "^_"` and `mangle_quoted: true`: ```js obj[`_field`] = 1; obj._field = 2; ``` becomes: ```js obj[`e`] = 1; obj.e = 2; ``` ### Limitation - Property mangling is unsafe when other code still uses the old property name. Public APIs and names shared with external code, host APIs, imported namespace objects, or separate builds must be excluded or reserved. - Quote handling is per use. Without `mangle_quoted`, `obj._field` may change while `obj["_field"]` does not. - The mangler cannot find names built at runtime or stored in arbitrary strings. Code that uses `eval`, `Function`, or `with` must reserve any affected names. - Compression may create a matching property after this pass has finished. For example, `'f' + 'oo_'` may later become `foo_` and will not be mangled. - TypeScript enum reverse mappings are not supported because the transform creates quoted strings. - Multi-program support is a caller API. The caller must collect every program that shares mangled properties before assigning names. The public `oxc-minify` API is stacked in #24741. The downstream multi-chunk integration is shown in rolldown/rolldown#10374. Supersedes #23772. AI assistance: OpenAI Codex and Claude were used for research, implementation, review, and testing. The contributor is responsible for reviewing and understanding the changes before submission.
12 KiB
Minifier Assumptions
The Oxc minifier makes certain assumptions about JavaScript code to achieve optimal compression. These assumptions are standard for typical JavaScript but may not hold for unusual code patterns.
These assumptions are validated using ECMAScript operations from oxc_ecmascript, which implements spec-compliant behavior for type conversions, side effect analysis, and constant evaluation.
Core Assumptions
These assumptions are held regardless of the options.
No Monkey-Patching Built-ins
Built-in objects and their methods and properties behave as specified in ECMAScript.
// The minifier assumes this never happens:
Array.prototype.push = function () {
console.log("hijacked!");
};
Object.defineProperty(Number.prototype, "toString", { value: () => "hijacked!" });
No document.all Usage
The deprecated document.all with its special typeof behavior is not used.
// The minifier assumes this never happens:
typeof document.all === "undefined"; // true in browsers
document.all && console.log("exists but falsy");
No with Statement
Code does not use with statements which create ambiguous scope.
// The minifier assumes this never happens:
with (obj) {
x = 1; // Is this obj.x or a global x?
}
Coercion Methods Are Pure
.toString(), .valueOf(), and [Symbol.toPrimitive]() have no side effects. The minifier uses oxc_ecmascript type conversion utilities that assume standard coercion behavior.
// The minifier assumes this never happens:
const obj = {
toString() {
console.log("side effect!");
return "";
},
};
String(obj); // Would trigger side effect
No TDZ Violation
Code doesn't violate Temporal Dead Zones.
// The minifier assumes this never happens:
console.log(x); // TDZ violation
let x = 1;
No errors from Array/String/TypedArray Maximum Length
Creating strings, arrays, or typed arrays that exceed maximum length can be moved or removed.
// The minifier may change when this error occurs:
try {
new Array(2 ** 32); // RangeError
} catch {
console.log("caught");
}
This also lets an unused new Int8Array(n) (or any TypedArray) with a numeric-literal n be dropped: a valid length allocates a zeroed buffer with no observable effect, and a too-large literal only throws a maximum-length RangeError. A non-literal, negative (new Int8Array(-1)), BigInt (new Int8Array(0n)), or object argument is kept.
Coercion to a primitive does not throw for non-Symbol arguments
Pure global functions and methods that coerce their arguments (parseInt, isNaN, Math.*, Date.UTC, String.fromCharCode, …) are treated as side-effect-free, so an unused call is dropped. A Symbol argument would make ToString / ToNumber throw a TypeError, but the minifier assumes a non-Symbol argument when it cannot prove the type — so Math.abs(x) / parseInt(x) stay droppable.
// The minifier assumes this never happens:
Math.abs(Symbol.iterator); // TypeError: Cannot convert a Symbol value to a number
A call is still kept when an argument provably throws:
ToNumberon aBigInt(Math.abs(10n),isNaN(10n),Date.UTC(10n)).String.fromCodePoint(cp)with a code point outside[0, 0x10FFFF](String.fromCodePoint(-1)) — an "invalid code point"RangeError, distinct from the droppable maximum-length errors above.- A missing or invalid required argument (
String.raw(),Symbol.keyFor(),URL.canParse()).
No side effects from extending a class
Extending a class does not have a side effect.
const v = [];
class A extends v {} // TypeError
Variables declared in direct eval are not referenced outside the eval
Variables declared in direct eval are not referenced outside the eval, which is only allowed in non-strict mode.
// The minifier assumes this never happens:
eval("var x = 1");
console.log(x); // 1
Known globals exist and are side-effect-free to access
Accessing known global identifiers (e.g. Math, Array, Object, console, document, window, DOM classes, etc.) does not throw a ReferenceError and has no side effects. Reading their properties (e.g. Math.PI, Object.keys) and select 3-level chains (e.g. Object.prototype.hasOwnProperty) are also side-effect-free. This list is ported from Rolldown's GLOBAL_IDENT set, which mirrors Rollup's knownGlobals, and includes browser/host-specific APIs intentionally.
// The minifier assumes these are always available:
Math.PI; // side-effect-free
Array.isArray; // side-effect-free
console; // side-effect-free to access (calling methods may still have side effects)
No side effects from accessing to a global variable named arguments
Accessing a global variable named arguments does not have a side effect. We intend to change this assumption to optional in the future.
// The minifier assumes this never happens:
console.log(arguments); // ReferenceError: arguments is not defined
Property writes to provably-unused local bindings are effect-free
In full-minify mode, a plain = write to a property of a local binding is
dropped when the binding is provably unused: it holds a fresh value (object /
array / function / class literal with no getters, setters, or __proto__), is
not exported, and every reference to it is itself a property-write target
(terser parity — function A() {} A.from = () => {}; is removed entirely).
This assumes Object.prototype / Function.prototype / Array.prototype
properties are not setters with side effects that such writes would trigger (the
same class of assumption as the property_write_side_effects: false tree-shaking
option, but applied only to this narrow provably-unused case). This is the only
unsoundness in this optimization.
Writes that would themselves throw a strict-mode TypeError or have an
observable value-domain effect are kept via a kind-aware key denylist: the
non-writable own name / length and the caller / arguments poison of a
function or class, a class's non-writable prototype, an instance-private
brand-check write (o.#x = 1), and an Array length write (which can throw a
RangeError or run a valueOf coercion). A plain function's prototype is
writable, so f.prototype = {...} still drops.
Writes are NOT dropped when any other operation on the binding could observe
them: compound / logical assignments and updates (o.x += 1 reads the
property), chained writes (a.b.c = 1 reads a.b), delete of a nested
member, or writes through __proto__ / non-literal computed keys (which may
install setters).
// The minifier assumes this never happens:
Object.defineProperty(Object.prototype, "x", {
set() {
console.log("side effect!");
},
});
var o = {};
o.x = 1; // dropped — would have logged
Function.prototype.toString is not relied on
Code does not depend on Function.prototype.toString() returning specific source text. Minification renames variables and parameters, simplifies expressions (true → !0), restructures statements (fusing with the comma operator, converting while to for), removes whitespace, and may eliminate function bodies entirely (e.g. IIFE inlining). All of these change the string returned by .toString().
const serialize = (fn) => fn.toString();
serialize((x) => {
return x * 2;
}); // Relied on to contain "return"
Optional Assumptions
Property names selected for mangling are not accessed dynamically
When property-name mangling is enabled, code does not access selected properties through an
arbitrary runtime string. Quoted syntax is handled per occurrence: it remains unchanged unless
mangle_quoted is enabled, while an unquoted occurrence of the same spelling may be renamed.
No-substitution template keys follow the same quoted rule; interpolated templates are not
rewritten.
obj._field; // eligible when `include` matches
obj["_field"]; // kept unless quoted mangling is enabled
obj[`_field`]; // also kept unless quoted mangling is enabled
obj[keyFromNetwork]; // cannot be updated safely
Use exclude, reserved, or a false cache entry for public/reflected names. Properties owned by
unminified code, imported module namespaces, globals, DOM objects, or other host APIs must also be
excluded or reserved. A leading /* @__KEY__ */ or /* #__KEY__ */ annotation marks a string or
no-substitution template as a property name. Numeric spellings, __proto__, constructor, and
prototype are never mangled.
Property mangling does not inspect source strings evaluated by direct eval or the Function
constructor, and it does not bail out inside a with statement. Matching names reached through
these dynamic mechanisms must be excluded or reserved explicitly.
Property rewriting runs before compression. Computed keys that compression later materializes,
such as ['f' + 'oo_'], are not revisited and can diverge from an eligible obj.foo_ occurrence.
Exclude or reserve property names that are constructed this way.
No Reliance on Function.prototype.name
Code doesn't depend on function names being preserved. This assumption is held by default. This can be changed by settings keepNames option.
// The minifier assumes this never happens:
function myFunc() {}
if (myFunc.name !== "myFunc") throw Error();
Configuration
Optional assumptions can be configured in the minifier options if your code requires different behavior.
pub struct CompressOptions {
// Control optimization behavior
pub drop_console: bool,
pub drop_debugger: bool,
pub join_vars: bool,
pub sequences: bool,
pub unused: CompressOptionsUnused,
pub keep_names: CompressOptionsKeepNames,
// Tree-shaking options affect side effect analysis
pub treeshake: TreeShakeOptions,
}
pub struct TreeShakeOptions {
// Whether property reads have side effects
pub property_read_side_effects: PropertyReadSideEffects,
// Whether property writes have side effects
pub property_write_side_effects: bool,
// Whether accessing unknown globals has side effects
pub unknown_global_side_effects: bool,
// Respect pure annotations like /* @__PURE__ */
pub annotations: bool,
}
Validation
To ensure your code works with these assumptions:
- Test thoroughly: Run your test suite on minified code
- Use conformance tests:
cargo coverageruns test262, Babel, TypeScript tests - Node.js compatibility: The minifier is validated against Node.js compatibility tables
- Try real-world code: Test with your actual application
- Report issues: If valid code breaks, file an issue
Node.js Compatibility Testing
The minifier includes comprehensive validation against Node.js compatibility tables to ensure optimizations work correctly across different Node.js versions and environments.
When Assumptions Don't Hold
If your code violates these assumptions, you may:
- Refactor code to follow standard patterns
- Disable specific optimizations
- Use a different minifier configuration
- Report the issue for consideration