mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 18:43:44 +00:00
feat(minifier): treeshake pure typed arrays and Set/Map array literals (#23469)
## Summary Mark side-effect-free `new` constructions pure so they are dropped when unused (and annotated for downstream tree-shaking): - **TypedArrays** with a non-negative numeric-literal length (`new Int8Array(8)`, `new Uint8Array(1024)`). The buffer allocation runs no user code; a valid length is pure and a too-large length only throws a maximum-length `RangeError`, which the minifier is allowed to drop (see `docs/ASSUMPTIONS.md`). `new Int8Array(-1)` (negative-length `RangeError`), `new Int8Array(0n)` (BigInt `TypeError`), object, and non-literal arguments are kept. - **`new Set([...])` / `new Map([[..], ..])`** with array-literal arguments; element side effects are preserved when the construction is dropped. `WeakSet`/`WeakMap` are excluded (their keys must be objects, so `new WeakSet([1])` throws), and `Map` entries must be array literals (`new Map([1])` throws). This matches esbuild's per-constructor handling — the only other minifier that keeps every throwing form; Rollup, Terser (`unsafe`), and SWC drop some of these incorrectly. Cross-verified against the ES spec, esbuild, Terser, Rollup, and SWC. Minified-size impact: react.development.js −0.04 kB; size-neutral elsewhere (real code uses dynamic buffer lengths, not numeric literals).
This commit is contained in:
1 parent
3fa8051d15
commit
53509a85df
9 files changed
+99
-24
No files matched your search
@@ -2,7 +2,7 @@
|
||||
// To edit this generated file you have to edit `tasks/ast_tools/src/generators/estree_visit.rs`.
|
||||
|
||||
/** Mapping from node type name to node type ID */
|
||||
export const NODE_TYPE_IDS_MAP = new Map([
|
||||
export const NODE_TYPE_IDS_MAP = /* @__PURE__ */ new Map([
|
||||
["DebuggerStatement", 0],
|
||||
["EmptyStatement", 1],
|
||||
["Literal", 2],
|
||||
|
||||
@@ -101,7 +101,7 @@ pub(super) fn is_error_constructor(name: &str) -> bool {
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
pub(super) fn is_typed_array_constructor(name: &str) -> bool {
|
||||
pub fn is_typed_array_constructor(name: &str) -> bool {
|
||||
matches!(name, "Int8Array" | "Uint8Array" | "Uint8ClampedArray"
|
||||
| "Int16Array" | "Uint16Array" | "Int32Array" | "Uint32Array"
|
||||
| "Float32Array" | "Float64Array" | "BigInt64Array" | "BigUint64Array")
|
||||
|
||||
@@ -5,7 +5,7 @@ mod pure_function;
|
||||
mod statements;
|
||||
|
||||
pub use context::{MayHaveSideEffectsContext, PropertyReadSideEffects};
|
||||
pub use known_globals::is_valid_regexp;
|
||||
pub use known_globals::{is_typed_array_constructor, is_valid_regexp};
|
||||
pub use pure_function::is_pure_function;
|
||||
|
||||
/// Returns true if subtree changes application state.
|
||||
|
||||
@@ -66,9 +66,9 @@ console.log(x); // TDZ violation
|
||||
let x = 1;
|
||||
```
|
||||
|
||||
### No errors from Array/String Maximum Length
|
||||
### No errors from Array/String/TypedArray Maximum Length
|
||||
|
||||
Creating strings or arrays that exceed maximum length can be moved or removed.
|
||||
Creating strings, arrays, or typed arrays that exceed maximum length can be moved or removed.
|
||||
|
||||
```javascript
|
||||
// The minifier may change when this error occurs:
|
||||
@@ -79,6 +79,8 @@ try {
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
### No side effects from extending a class
|
||||
|
||||
Extending a class does not have a side effect.
|
||||
|
||||
@@ -3,7 +3,7 @@ use oxc_allocator::{TakeIn, Vec};
|
||||
use oxc_ast::ast::*;
|
||||
use oxc_ecmascript::{
|
||||
constant_evaluation::{DetermineValueType, ValueType},
|
||||
side_effects::is_valid_regexp,
|
||||
side_effects::{is_typed_array_constructor, is_valid_regexp},
|
||||
};
|
||||
use oxc_semantic::IsGlobalReference;
|
||||
use oxc_syntax::scope::ScopeFlags;
|
||||
@@ -355,6 +355,30 @@ impl<'a> Normalize {
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Typed arrays allocate a zeroed buffer and run no user code when the
|
||||
// length argument is a non-negative numeric literal: it is either a valid
|
||||
// length (pure) or too large, which throws a maximum-length `RangeError`
|
||||
// that the minifier is allowed to drop (see `docs/ASSUMPTIONS.md`). The
|
||||
// value must be checked explicitly because constant folding can turn `-1`
|
||||
// into a negative-valued `NumericLiteral`. Other arguments are kept:
|
||||
// `new Int8Array(-1)` throws a negative-length `RangeError`,
|
||||
// `new Int8Array(0n)` throws a `TypeError` (BigInt), and an object argument
|
||||
// can run user code via `Symbol.iterator` / `valueOf`. The 0-arg and
|
||||
// `0`-literal forms (the latter folded to 0-arg by
|
||||
// `substitute_typed_array_constructor`) are both covered, so the result is
|
||||
// idempotent regardless of fold order.
|
||||
name if is_typed_array_constructor(name) => {
|
||||
let safe_length = new_expr.arguments.is_empty()
|
||||
|| (new_expr.arguments.len() == 1
|
||||
&& matches!(
|
||||
new_expr.arguments[0].as_expression(),
|
||||
Some(Expression::NumericLiteral(lit)) if lit.value >= 0.0
|
||||
));
|
||||
if safe_length && Self::can_set_pure(ident, ctx) {
|
||||
new_expr.pure = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
|
||||
@@ -366,7 +390,28 @@ impl<'a> Normalize {
|
||||
0 if !zero_arg_throws_error => true,
|
||||
1 => match new_expr.arguments[0].as_expression() {
|
||||
Some(Expression::ArrayExpression(array_expr)) => {
|
||||
array_expr.elements.is_empty() && !one_arg_array_throws_error
|
||||
if one_arg_array_throws_error {
|
||||
false
|
||||
} else if array_expr.elements.is_empty() {
|
||||
true
|
||||
} else {
|
||||
// A non-empty array literal is iterated via the built-in
|
||||
// array iterator (side-effect-free; element side effects are
|
||||
// preserved when the call is dropped). Only `Set`/`Map` accept
|
||||
// arbitrary entries: `Set` takes any values; `Map` requires
|
||||
// every entry to be an array literal (`new Map([1])` throws —
|
||||
// `1` is not iterable). `WeakSet`/`WeakMap` are excluded —
|
||||
// their keys must be objects, so `new WeakSet([1])` /
|
||||
// `new WeakMap([[1, 2]])` throw.
|
||||
match ident.name.as_str() {
|
||||
"Set" => true,
|
||||
"Map" => array_expr
|
||||
.elements
|
||||
.iter()
|
||||
.all(|el| matches!(el, ArrayExpressionElement::ArrayExpression(_))),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(e) => {
|
||||
if let Expression::NewExpression(new_expr) = e {
|
||||
|
||||
@@ -119,18 +119,40 @@ fn test_new_constructor_side_effect() {
|
||||
test("new Date(undefined)", "");
|
||||
test_same("new Date(x)");
|
||||
test("new Set()", "");
|
||||
// test("new Set([a, b, c])", "");
|
||||
test("new Set([1, 2, 3])", "");
|
||||
// Element side effects are preserved when the pure construction is dropped.
|
||||
test("new Set([foo(), bar()])", "foo(), bar();");
|
||||
test("new Map([[foo(), bar()]])", "foo(), bar();");
|
||||
test("new Set(null)", "");
|
||||
test("new Set(undefined)", "");
|
||||
test("new Set(void 0)", "");
|
||||
test_same("new Set(x)");
|
||||
test("new Map()", "");
|
||||
test("new Map([[1, 2], [3, 4]])", "");
|
||||
test("new Map(null)", "");
|
||||
test("new Map(undefined)", "");
|
||||
test("new Map(void 0)", "");
|
||||
// test_same("new Map([x])");
|
||||
test_same("new Map(x)");
|
||||
// test("new Map([[a, b], [c, d]])", "");
|
||||
// Map entries must be array literals, otherwise they are not iterable and throw.
|
||||
test_same("new Map([x])");
|
||||
test_same("new Map([1, 2])");
|
||||
// WeakSet/WeakMap keys must be objects, so array-literal args throw.
|
||||
test_same("new WeakSet([1])");
|
||||
test_same("new WeakMap([[1, 2]])");
|
||||
// Typed arrays allocate a zeroed buffer with no user code for a numeric-literal
|
||||
// length: a valid length is pure, and a too-large length is a max-length
|
||||
// RangeError the minifier is allowed to drop (see docs/ASSUMPTIONS.md).
|
||||
test("new Int8Array()", "");
|
||||
test("new Uint8Array()", "");
|
||||
test("new Int8Array(8)", "");
|
||||
test("new Int8Array(1024)", "");
|
||||
// Kept: `-1` throws a negative-length RangeError, `0n` throws a TypeError, an
|
||||
// object arg can run user code, and a shadowed `Int8Array` is not the builtin.
|
||||
test_same("new Int8Array(-1)");
|
||||
test_same("new Int8Array(0n)");
|
||||
test_same("new Int8Array(x)");
|
||||
test_same("new Int8Array([1, 2])");
|
||||
test_same("var Int8Array; new Int8Array()");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -262,20 +262,26 @@ fn test_fold_new_expressions() {
|
||||
|
||||
#[test]
|
||||
fn test_compress_typed_array_constructor() {
|
||||
test("new Int8Array(0)", "new Int8Array()");
|
||||
test("new Uint8Array(0)", "new Uint8Array()");
|
||||
test("new Uint8ClampedArray(0)", "new Uint8ClampedArray()");
|
||||
test("new Int16Array(0)", "new Int16Array()");
|
||||
test("new Uint16Array(0)", "new Uint16Array()");
|
||||
test("new Int32Array(0)", "new Int32Array()");
|
||||
test("new Uint32Array(0)", "new Uint32Array()");
|
||||
test("new Float32Array(0)", "new Float32Array()");
|
||||
test("new Float64Array(0)", "new Float64Array()");
|
||||
test("new BigInt64Array(0)", "new BigInt64Array()");
|
||||
test("new BigUint64Array(0)", "new BigUint64Array()");
|
||||
// `new Int8Array(0)` is equivalent to `new Int8Array()`: the `0` arg is folded
|
||||
// away and the pure empty construction is dropped when its result is unused.
|
||||
test("new Int8Array(0)", "");
|
||||
test("new Uint8Array(0)", "");
|
||||
test("new Uint8ClampedArray(0)", "");
|
||||
test("new Int16Array(0)", "");
|
||||
test("new Uint16Array(0)", "");
|
||||
test("new Int32Array(0)", "");
|
||||
test("new Uint32Array(0)", "");
|
||||
test("new Float32Array(0)", "");
|
||||
test("new Float64Array(0)", "");
|
||||
test("new BigInt64Array(0)", "");
|
||||
test("new BigUint64Array(0)", "");
|
||||
test("new Int8Array()", "");
|
||||
// A numeric-literal length is pure (or a droppable max-length RangeError).
|
||||
test("new Int8Array(8)", "");
|
||||
|
||||
// Not optimized when shadowed, or with a negative / non-literal / extra length.
|
||||
test_same("var Int8Array; new Int8Array(0)");
|
||||
test_same("new Int8Array(1)");
|
||||
test_same("new Int8Array(-1)");
|
||||
test_same("new Int8Array(a)");
|
||||
test_same("new Int8Array(0, a)");
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// To edit this generated file you have to edit `tasks/ast_tools/src/generators/estree_visit.rs`.
|
||||
|
||||
/** Mapping from node type name to node type ID */
|
||||
export const NODE_TYPE_IDS_MAP = new Map([
|
||||
export const NODE_TYPE_IDS_MAP = /* @__PURE__ */ new Map([
|
||||
["DebuggerStatement", 0],
|
||||
["EmptyStatement", 1],
|
||||
["Literal", 2],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
| Oxc | ESBuild | Oxc | ESBuild |
|
||||
Original | minified | minified | gzip | gzip | Iterations | File
|
||||
-------------------------------------------------------------------------------------
|
||||
72.14 kB | 23.18 kB | 23.70 kB | 8.38 kB | 8.54 kB | 2 | react.development.js
|
||||
72.14 kB | 23.14 kB | 23.70 kB | 8.36 kB | 8.54 kB | 2 | react.development.js
|
||||
|
||||
173.90 kB | 59.40 kB | 59.82 kB | 19.15 kB | 19.33 kB | 2 | moment.js
|
||||
|
||||
|
||||
Reference in new issue
Block a user