Skip to content

feat(minifier): add property name mangling - #24740

Merged
graphite-app[bot] merged 1 commit into
mainfrom
codex/mangle-props-core
Aug 17, 2026
Merged

feat(minifier): add property name mangling#24740
graphite-app[bot] merged 1 commit into
mainfrom
codex/mangle-props-core

Conversation

@Dunqing

@Dunqing Dunqing commented Jul 21, 2026

Copy link
Copy Markdown
Member

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:

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:

obj[`_field`] = 1;
obj._field = 2;

becomes:

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.

@codspeed-hq

codspeed-hq Bot commented Jul 21, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 67 untouched benchmarks
🆕 5 new benchmarks
⏩ 9 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
🆕 Simulation property_mangler[App.tsx] N/A 8.2 ms N/A
🆕 Simulation property_mangler[react.development.js] N/A 1.4 ms N/A
🆕 Simulation property_mangler[binder.ts] N/A 1.9 ms N/A
🆕 Simulation property_mangler[kitchen-sink.tsx] N/A 18.4 ms N/A
🆕 Simulation property_mangler[RadixUIAdoptionSection.jsx] N/A 177.1 µs N/A

Comparing codex/mangle-props-core (5f08776) with main (63ff8ef)

Open in CodSpeed

Footnotes

  1. 9 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@Dunqing
Dunqing force-pushed the codex/mangle-props-core branch 2 times, most recently from e9070cb to 75c9e72 Compare July 28, 2026 05:46
@Dunqing
Dunqing force-pushed the codex/mangle-props-core branch 11 times, most recently from 6a6957b to abf6ec8 Compare August 11, 2026 03:57
@Dunqing
Dunqing marked this pull request as ready for review August 11, 2026 06:05
@Dunqing
Dunqing requested review from Boshen and sapphi-red August 11, 2026 06:06
@Dunqing

Dunqing commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

I think this PR is ready to review; this new implementation resolved all reviews of prior attempted in #23772.

@Boshen Boshen self-assigned this Aug 11, 2026

@Boshen Boshen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you verify against test262 runtime tests?

@Boshen Boshen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description, comments, and documentation require work to enable maintainers to understand how this feature works.

@Boshen Boshen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we enable this in cargo minsize?

@sapphi-red

Copy link
Copy Markdown
Member

Would you add the reason why this lives in oxc_minifier rather than oxc_mangler to the PR description?

Comment thread crates/oxc_minifier/src/property_mangler.rs
@Dunqing

Dunqing commented Aug 13, 2026

Copy link
Copy Markdown
Member Author

Added this rationale to the PR description. Property mangling must inspect and rewrite property-bearing AST syntax before compression erases quote and key context. oxc_mangler is the scope-aware identifier mangler and supplies symbol/private-member names to codegen, so this AST-rewrite phase belongs in oxc_minifier.

@Dunqing

Dunqing commented Aug 13, 2026

Copy link
Copy Markdown
Member Author

Enabled property mangling in both test262 runtime and minsize and all look good.

@Dunqing
Dunqing force-pushed the codex/mangle-props-core branch 3 times, most recently from c0edad4 to 599c9be Compare August 14, 2026 07:09
@Dunqing
Dunqing requested a review from sapphi-red August 17, 2026 02:23

@sapphi-red sapphi-red left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the transformer keep /* #__KEY__ */? If not, I guess that would be needed.

Comment thread crates/oxc_minifier/tests/mangler/property_mangler.rs
@Dunqing
Dunqing force-pushed the codex/mangle-props-core branch from 599c9be to 5f08776 Compare August 17, 2026 06:19
@Dunqing

Dunqing commented Aug 17, 2026

Copy link
Copy Markdown
Member Author

Does the transformer keep /* #__KEY__ */? If not, I guess that would be needed.

Good catch. The transformer won't touch comments, but currently, codegen will miss printing comments in some places, especially for some inline comments. I will check and fill the gap after the merge.

@Dunqing

Dunqing commented Aug 17, 2026

Copy link
Copy Markdown
Member Author

Merging it, the comments issue was worked on #25766

@Dunqing Dunqing added the 0-merge Merge with Graphite Merge Queue label Aug 17, 2026

Dunqing commented Aug 17, 2026

Copy link
Copy Markdown
Member Author

Merge activity

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.
@graphite-app
graphite-app Bot force-pushed the codex/mangle-props-core branch from 5f08776 to 2f5cdb1 Compare August 17, 2026 07:50
@graphite-app
graphite-app Bot merged commit 2f5cdb1 into main Aug 17, 2026
32 checks passed
@graphite-app graphite-app Bot removed the 0-merge Merge with Graphite Merge Queue label Aug 17, 2026
@graphite-app
graphite-app Bot deleted the codex/mangle-props-core branch August 17, 2026 07:55
camc314 added a commit to camc314/rolldown that referenced this pull request Aug 17, 2026
oxc-project/oxc#24740

Oxc-Revision: 2f5cdb1231d217ba33d0c0ee777fa36095c939b4
graphite-app Bot pushed a commit that referenced this pull request Aug 18, 2026
The property mangler added in #24740 is not available through `oxc-minify`. This adds `mangleProps` for configuring it and `mangleCache` for reusing chosen property names.

### Feature

- `include` selects property names to mangle. `exclude` and `reserved` keep names unchanged.
- `quoted` also mangles quoted property names. `debug` produces readable output names.
- `cache` fixes a property to a chosen name. A `false` value keeps the original name.
- Property mangling works in both sync and async APIs. It is separate from identifier mangling.
- When property mangling is enabled and the call succeeds, the result includes the updated `mangleCache`.
- Invalid `mangleProps` regexes and cache entries are returned in `result.errors`.

### Design

The binding converts JavaScript values into core minifier options and results. The core minifier still chooses names and rewrites properties, so both APIs follow the same rules.

A parser error may leave an incomplete AST. In that case, property mangling is skipped and no partial cache is returned.

### Example

```js
const result = minifySync(
  "test.js",
  "let local; obj._often; obj._rare; obj._often;",
  {
    compress: false,
    mangle: false,
    mangleProps: { include: "^_" },
  },
);

result.code; // "let local;obj.e;obj.t;obj.e;"
result.mangleCache; // { _often: "e", _rare: "t" }
```

### Limitation

- `include` and `exclude` use Rust regex syntax. They match anywhere unless the pattern includes anchors.
- Do not reuse the cache on already-mangled output. A mapping is not always safe to apply twice.
- The cache only remembers recorded mappings. It cannot find unchanged names in another input or in external code. Reserve, exclude, or pin those names before the first call.
- String cache targets must be valid JavaScript `IdentifierName` values and cannot be `__proto__`, `constructor`, or `prototype`. The original name `__proto__` cannot be a cache key.

AI assistance: OpenAI Codex and Claude were used for research, implementation, adversarial review, and verification. The contributor remains responsible for reviewing, understanding, and submitting the changes under the repository AI usage policy.
camc314 added a commit to camc314/rolldown that referenced this pull request Aug 18, 2026
oxc-project/oxc#24740

Oxc-Revision: 2f5cdb1231d217ba33d0c0ee777fa36095c939b4
Boshen added a commit that referenced this pull request Aug 18, 2026
### 💥 BREAKING CHANGES

- 365274e packages/codegen: [**BREAKING**] `printSync` return an object
(#25720) (overlookmotel)

### 🚀 Features

- 6a7eb60 packages/codegen: Alter capitalization of `sourceFilename`
option (#25854) (overlookmotel)
- 300763f packages/codegen: Add full source map support (#25585)
(camc314)
- a169e4a napi/minify: Expose property name mangling options (#24741)
(Dunqing)
- 2f5cdb1 minifier: Add property name mangling (#24740) (Dunqing)
- 4922141 mangler: Deduplicate private accessor names (#25601) (camc314)
- 1c4f519 napi: Add Relay transform plugin (#25503) (Boshen)
- a4478e9 codegen: Add `oxc-codegen` package (#25488) (overlookmotel)

### 🐛 Bug Fixes

- 0fbcf64 codegen: Validate sourcemap options (#25860) (camc314)
- 345f981 react_compiler: Skip node_modules by default (#25859) (Boshen)
- ab81f3f minifier: Drop side-effect-free additions (#25639) (Dunqing)
- 241c559 napi/minify: Accept RegExp for property filters (#25827)
(Dunqing)
- 4cc7ea4 codegen: Reject invalid indent options (#25807) (camc314)
- 53f9295 isolated-declarations: Preserve undefined for defaulted any
(#25292) (camc314)
- 8ab883a codegen: Preserve property key annotations (#25766) (Dunqing)
- b13fd48 minifier: Model uninitialized module vars as undefined
(#25497) (Dunqing)
- f6000ac ecmascript: Fold `**` with `Number::exponentiate`, not IEEE
`pow` (#25644) (Kotaro Chikuba)
- b846abc isolated-declarations: Handle ambient expando properties
(#25655) (camc314)
- dec0a86 react-compiler: Standardize diagnostics (#25702) (Boshen)
- fca2e0c parser: Reject initialized lexical declarations in for-in
(#25700) (Boshen)
- ae9be8f parser: Forbid type parameters on quoted constructors (#25696)
(Boshen)
- c5f188a react-compiler: Use Babel v1 validation defaults (#25676)
(Boshen)
- db44651 napi: Disable reuseWorker in browser bindings (#25640)
(leaysgur)
- ce35d47 react_compiler: Preserve JSX import source pragmas (#25592)
(Boshen)
- 059784d semantic: Respect shadowed `Infinity` and `NaN` in enum
evaluation (#25604) (camc314)
- bb5a232 minifier: Keep variable declaration initilized with class when
keepNames is enabled (#25584) (sapphi-red)
- f49229d minifier: Keep side effects when rotating bitwise operands
(#25596) (Kotaro Chikuba)
- e82495b ecmascript: Derive `ToNumber` of `!x` from `ToBoolean`
(#25595) (Kotaro Chikuba)
- 509931b semantic: Classify global references per identifier (#25608)
(camc314)
- 80484ce mangler: Correct base54 safety comment (#25606) (camc314)
- c1369a7 codegen: Resolve private names in class heritage (#25588)
(camc314)
- c002f29 codegen: Escape sources for empty import specifiers (#25586)
(camc314)
- 0c68b7f estree: Emit `decorators` on `FormalParameterRest` (#25582)
(camc314)
- cdf1846 semantic: Allow legacy escapes in JSX attributes (#25576)
(Boshen)
- e75e102 minifier: Preserve block statement in labeled iteration
statements (#25162) (Armano)
- 771d79a mangler: Exclude non-manglable symbols from slot assignment
(#25539) (sapphi-red)
- 76b19f5 minifier: Avoid duplicating large folded strings (#25532)
(Dunqing)
- 59e8895 codegen: Validate starting indent level (#25550) (camc314)
- 88b34f2 clippy: Remove unneeded `unsafe` (#25551) (camc314)
- 23a7ad0 parser: Stop delimited lists at end of file (#25542) (Boshen)
- 73acba9 parser: Preserve fatal errors during await reparse (#25541)
(Boshen)
- c3e99d1 minifier: Avoid invalid octal escapes in template folds
(#25495) (Dunqing)
- b4e6a9e codegen: Output newline after `export default interface`
(#25487) (overlookmotel)
- 5fcf683 minifier: Correct issue with try finally termination (#25185)
(Armano)
- 1645d93 react_compiler: Preserve source spans (#25462) (Boshen)
- 8d7f9cf react_compiler: Honor eslint suppressions (#25394) (Boshen)

### ⚡ Performance

- 568203e ecmascript: Use binary search for known globals (#25817)
(Boshen)
- c84ede3 estree_tokens: Share JS token update entry point (#25826)
(Boshen)
- 9a6e862 minifier: Move owned statements directly (#25835) (Dunqing)
- 61b2aef minifier: Move owned AST nodes directly (#25837) (Dunqing)
- 673b04b minifier: Replace expressions without take_in dummies (#25836)
(Dunqing)
- 63ff8ef linter: Outline diagnostic construction (#25762) (Boshen)
- 621808e diagnostics: Measure graphemes lazily (#25723) (Boshen)
- f62ed0e diagnostics: Optimize graphical number rendering (#25715)
(Boshen)
- 7152834 diagnostics: Reduce graphical formatting overhead (#25714)
(Boshen)
- 10fc4b7 diagnostics: Reduce graphical rendering allocations (#25711)
(Boshen)
- 9c8abab diagnostics: Batch graphical reports (#25710) (Boshen)
- 757f3d4 mangler: Share allocated names across reused slots (#25605)
(camc314)
- 5444cbf codegen: Skip escaping harmless `<` tokens (#25564) (camc314)

### 📚 Documentation

- 58f7ab9 packages/codegen: Reformat docs and comments (#25848)
(overlookmotel)
- 627466e transform-react: Document all options (#25800) (Boshen)
- ce03ac1 packages/codegen: Fix JSDoc comments for `printSync` (#25722)
(overlookmotel)
- 386a699 packages/codegen: Correct JSDoc comment (#25716)
(overlookmotel)
- 31e571d mangler: Update code example (#25599) (camc314)
- fd62354 codegen: Revamp package readme (#25560) (camc314)
- ffa3153 codegen: Clarify raw transfer Node requirement (#25565)
(camc314)
- c5d4063 codegen: Clarify binary walk allocations (#25562) (camc314)
- 5703d7a codegen: Correct stale printer comments (#25561) (camc314)

Co-authored-by: Boshen <1430279+Boshen@users.noreply.github.com>
camc314 added a commit to camc314/rolldown that referenced this pull request Aug 18, 2026
oxc-project/oxc#24740

Oxc-Revision: 2f5cdb1231d217ba33d0c0ee777fa36095c939b4
camc314 added a commit to camc314/rolldown that referenced this pull request Aug 19, 2026
oxc-project/oxc#24740

Oxc-Revision: 2f5cdb1231d217ba33d0c0ee777fa36095c939b4
@spaceemotion

Copy link
Copy Markdown

I wonder... is there a way to have the minifier auto-mangle properties in classes that are never exposed outside a module? e.g.:

// reader.ts

class FooReader {
  read(text: string) {}
}

// The only method that gets exported from the module, the class is never exposed
export const readThing = (text: string) => {
  return new FooReader().read(text);
}

Having to configure properly mangling rules kind of feels like I now need to know the inner workings of library/package code instead of the bundler knowing that something can be mangled already 🤔

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-ast-tools Area - AST tools A-minifier Area - Minifier

4 participants