mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
feat(minifier): inline const value for read-only vars (#22593)
## Summary The minifier skipped constant-inlining for **every** `var`, blaming TDZ. The real constraint is narrower — `var` *hoisting* — and most `var`s are still safe to inline. This lifts the blanket skip and inlines a `var`'s constant whenever no read can observe its hoisted `undefined`. Closes #13051. ## Why A `var x = <literal>;` binds `x` to `undefined` at the top of its scope and assigns the literal only in source order. A read in that window sees `undefined`, so inlining the literal everywhere would change behavior there — which is why the old code skipped all `var`s. But that also blocked a common, perfectly safe pattern from optimizing in DCE mode: ```js var DEBUG = false; function check() { if (DEBUG) console.log('debug'); } console.log(check()); ``` vite 8's `topLevelVar: true` rewrites top-level `let`/`const` to `var`, so users' flag patterns silently stopped tree-shaking — vitejs/vite#22363 → rolldown/rolldown#9279. ## The safety principle Inline only when the value at the read is provably the assigned literal, never the hoisted `undefined`. Concretely, no user code may run between scope entry and the assignment that could read `x`. A `var x = <literal>;` qualifies when all of: - **It is in a still-declarative body.** It sits directly in the current function/program body, and every preceding statement is *declarative* — runs no user code (`is_declarative_body_statement`: function/empty/type-only declarations, module re-exports, and literal-initialised simple `var`s). The first non-declarative statement ends the body's "declarative prelude"; this is also enforced per-declarator for multi-declarator statements. - **Its initializer is a `ConstantValue`.** - **It is not a top-level script var** (those alias the global object). - **At program scope, the module loads no other module** (`import` / `export … from` / `export * from`) — a cyclic importer could call into our exports and observe a captured var before assignment, regardless of export status. - **It has exactly one read, crossing a function boundary** — the gap that `substitute_single_use_symbol` (same call frame) and the small-value inliner don't fill. This also keeps the change byte-positive: dropping `var x = L;` and replacing one read with `L` always saves bytes. The prelude flag is a per-body stack pushed/popped at function boundaries, so the analysis runs independently for nested functions. Reassignment is handled by the existing `write_references_count` guard, and direct-eval scopes are excluded. **Verification.** A battery of hoisting / closure / reassignment programs was run through the minifier and then *executed*, checking every output reproduces the original's observable behavior. All pass — output is semantically equal to source on each, matching Terser. ## Comparison with other minifiers Behaviour checked against Terser 5.48, esbuild 0.24, and SWC 1.15: - **esbuild** only propagates `const` primitives — it never inlines a `var` cross-scope. So `const DEBUG = false; …` folds to `console.log(void 0)` but `var DEBUG = false; …` stays. That is exactly the regression path: `topLevelVar: true` rewrites `const`→`var`, defeating const-only propagation. oxc had the same blanket-`var` skip; this PR lifts it for the provably-safe subset. - **Terser / SWC** reach the same result (and more — flow-sensitive reassignment, call inlining) via multi-pass `reduce_vars`/`collapse_vars`. This PR recovers the common flag pattern with one syntactic prelude check instead of that machinery, staying single-pass. ## Results `minsize` shows small gzip improvements on lodash / victory / antd / typescript; jquery is byte-identical. Closes #13051
This commit is contained in:
1 parent
bcb3894d76
commit
09762d9224
6 files changed
+406
-17
No files matched your search
@@ -2,6 +2,7 @@ use crate::generated::ancestor::Ancestor;
|
||||
use oxc_ast::ast::*;
|
||||
use oxc_ecmascript::constant_evaluation::{ConstantEvaluation, ConstantValue};
|
||||
use oxc_span::GetSpan;
|
||||
use oxc_syntax::{scope::ScopeId, symbol::SymbolId};
|
||||
|
||||
use crate::TraverseCtx;
|
||||
|
||||
@@ -11,9 +12,12 @@ impl<'a> PeepholeOptimizations {
|
||||
pub fn init_symbol_value(decl: &VariableDeclarator<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
let BindingPattern::BindingIdentifier(ident) = &decl.id else { return };
|
||||
let Some(symbol_id) = ident.symbol_id.get() else { return };
|
||||
let value = if decl.kind.is_var() || Self::is_for_statement_init(ctx) {
|
||||
// - Skip constant value inlining for `var` declarations, due to TDZ problems.
|
||||
// - Set None for for statement initializers as the value of these are set by the for statement.
|
||||
let value = if Self::is_for_statement_init(ctx) {
|
||||
// for-statement initializers have their value set by the for statement itself.
|
||||
None
|
||||
} else if decl.kind.is_var() && !Self::is_hoisted_var_inlineable(decl, symbol_id, ctx) {
|
||||
// `var` is hoisted: reads before the initializer line see `undefined`.
|
||||
// Skip unless the safety predicate proves no such read exists.
|
||||
None
|
||||
} else {
|
||||
decl.init.as_ref().map_or(Some(ConstantValue::Undefined), |e| e.evaluate_value(ctx))
|
||||
@@ -22,6 +26,64 @@ impl<'a> PeepholeOptimizations {
|
||||
ctx.init_value(symbol_id, value, is_fresh_value);
|
||||
}
|
||||
|
||||
/// Predicate for inlining a hoisted `var x = <literal>;`. True when no read
|
||||
/// can observe `x` as its hoisted `undefined`:
|
||||
/// - the declarator sits at the current body's top scope and that body is
|
||||
/// still in its declarative prelude;
|
||||
/// - it has an initializer (uninitialized `var foo;` would inline to
|
||||
/// `undefined`, which churns existing tests for marginal benefit);
|
||||
/// - script-mode top-level vars are excluded (they alias the global object);
|
||||
/// - at program scope, if the module loads any other module (`import`,
|
||||
/// `export … from`, `export * from`), skip: a cyclic importer can call
|
||||
/// into our exports and observe any var our exported functions/classes
|
||||
/// close over, regardless of export status;
|
||||
/// - exactly one read, and it sits inside a nested function/arrow body
|
||||
/// (multi-use and same-call-frame reads are handled by
|
||||
/// `inline_identifier_reference`'s small-value rule or by
|
||||
/// `substitute_single_use_symbol`).
|
||||
///
|
||||
/// Limitation: the constant is recorded here at the declarator's exit, so a
|
||||
/// reader in a function declared *before* the var in source order has
|
||||
/// already been visited and won't be inlined. Safe but suboptimal; the
|
||||
/// common "flag declared at the top" pattern is unaffected.
|
||||
fn is_hoisted_var_inlineable(
|
||||
decl: &VariableDeclarator<'a>,
|
||||
symbol_id: SymbolId,
|
||||
ctx: &TraverseCtx<'a>,
|
||||
) -> bool {
|
||||
if decl.init.is_none() || Self::keep_top_level_var_in_script_mode(ctx) {
|
||||
return false;
|
||||
}
|
||||
// `body_unsafe` is set by a preceding non-declarative statement, and the
|
||||
// program root additionally starts unsafe when the module has loaders
|
||||
// (see `enter_program`) — so this one check covers the cyclic-import gate.
|
||||
let &(body_scope, body_unsafe) = ctx.state.body_unsafe_stack.last();
|
||||
if body_unsafe || ctx.current_scope_id() != body_scope {
|
||||
return false;
|
||||
}
|
||||
// Exactly one read, and it crosses a function boundary.
|
||||
let mut reads = ctx.scoping().get_resolved_references(symbol_id).filter(|r| r.is_read());
|
||||
let Some(read) = reads.next() else { return false };
|
||||
reads.next().is_none()
|
||||
&& Self::read_crosses_function_boundary(read.scope_id(), body_scope, ctx)
|
||||
}
|
||||
|
||||
/// True if the scope chain from `read_scope` to `body_scope` (exclusive of
|
||||
/// `body_scope`) crosses any `Function` scope. `substitute_single_use_symbol`
|
||||
/// only walks adjacent statements within the same call frame, so this is
|
||||
/// the gap our path fills.
|
||||
fn read_crosses_function_boundary(
|
||||
read_scope: ScopeId,
|
||||
body_scope: ScopeId,
|
||||
ctx: &TraverseCtx<'a>,
|
||||
) -> bool {
|
||||
let scoping = ctx.scoping();
|
||||
scoping
|
||||
.scope_ancestors(read_scope)
|
||||
.take_while(|&s| s != body_scope)
|
||||
.any(|s| scoping.scope_flags(s).is_function())
|
||||
}
|
||||
|
||||
/// Check if an expression creates a fresh value that cannot alias another binding
|
||||
/// and has no setters/getters that could trigger side effects on property writes.
|
||||
fn is_fresh_value_expression(expr: &Expression<'a>) -> bool {
|
||||
|
||||
@@ -70,6 +70,78 @@ impl<'a> PeepholeOptimizations {
|
||||
None
|
||||
}
|
||||
|
||||
/// A body-level statement is "declarative" if executing it cannot run user
|
||||
/// code that observes a subsequent hoisted `var x = <literal>;` as
|
||||
/// `undefined`. Module loaders (`import`, `export * from`, `export … from`)
|
||||
/// can evaluate foreign modules but only observe our bindings on an actual
|
||||
/// cycle — handled at program scope by starting the root prelude unsafe when
|
||||
/// the module has loaders (see `enter_program`).
|
||||
/// Type-only declarations (`type`, `interface`) are erased and never run.
|
||||
fn is_declarative_body_statement(stmt: &Statement<'a>) -> bool {
|
||||
match stmt {
|
||||
Statement::EmptyStatement(_)
|
||||
| Statement::ImportDeclaration(_)
|
||||
| Statement::ExportAllDeclaration(_) => true,
|
||||
// `export { foo }`, `export { foo } from './x'`, `export type T = …` —
|
||||
// no executable code at the statement itself. The cyclic-eval hazard
|
||||
// from a `from` source is gated separately at program scope (see
|
||||
// `enter_program`).
|
||||
Statement::ExportNamedDeclaration(e) => {
|
||||
e.declaration.as_ref().is_none_or(Self::is_declarative_declaration)
|
||||
}
|
||||
// `export default function() {}` is hoisted; `export default <expr>`
|
||||
// or `export default class C extends … {}` runs user code.
|
||||
Statement::ExportDefaultDeclaration(e) => {
|
||||
matches!(&e.declaration, ExportDefaultDeclarationKind::FunctionDeclaration(_))
|
||||
}
|
||||
// Bare declarations route through the shared classifier; anything else
|
||||
// (blocks, expressions, control flow) can run user code.
|
||||
_ => stmt.as_declaration().is_some_and(Self::is_declarative_declaration),
|
||||
}
|
||||
}
|
||||
|
||||
/// A `Declaration` runs no user code at evaluation: function/type/interface
|
||||
/// declarations are inert, and a `var`/`let`/`const` is declarative only when
|
||||
/// every declarator is a simple binding with a literal (or no) initializer.
|
||||
/// Classes, enums, and TS modules run user code, so they are not declarative.
|
||||
fn is_declarative_declaration(decl: &Declaration<'a>) -> bool {
|
||||
match decl {
|
||||
Declaration::FunctionDeclaration(_)
|
||||
| Declaration::TSTypeAliasDeclaration(_)
|
||||
| Declaration::TSInterfaceDeclaration(_) => true,
|
||||
Declaration::VariableDeclaration(decl) => {
|
||||
Self::is_declarative_variable_declaration(decl)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// A `VariableDeclaration` is declarative when every declarator is a simple
|
||||
/// `BindingIdentifier` (no destructuring / defaults / computed keys, all of
|
||||
/// which can run user code) with either no initializer or a primitive
|
||||
/// literal initializer.
|
||||
fn is_declarative_variable_declaration(decl: &VariableDeclaration<'a>) -> bool {
|
||||
decl.declarations.iter().all(Self::is_declarative_variable_declarator)
|
||||
}
|
||||
|
||||
/// Note: only AST `Literal`s qualify. Constant-but-non-literal initializers
|
||||
/// (`-1`, `void 0`, `1 + 2`) run no user code either, but conservatively end
|
||||
/// the prelude here — a missed optimization, never a correctness risk.
|
||||
fn is_declarative_variable_declarator(decl: &VariableDeclarator<'a>) -> bool {
|
||||
matches!(decl.id, BindingPattern::BindingIdentifier(_))
|
||||
&& decl.init.as_ref().is_none_or(Expression::is_literal)
|
||||
}
|
||||
|
||||
/// Mark the current function/program body as no longer in its declarative
|
||||
/// prelude. No-op if the flag is already set, or if `current_scope_id` is
|
||||
/// some inner scope (a block/for/etc.) — those don't end the prelude.
|
||||
fn mark_current_body_unsafe(ctx: &mut TraverseCtx<'a>) {
|
||||
let &(body_scope, body_unsafe) = ctx.state.body_unsafe_stack.last();
|
||||
if !body_unsafe && body_scope == ctx.current_scope_id() {
|
||||
ctx.state.body_unsafe_stack.last_mut().1 = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if a member expression's base object may be mutated.
|
||||
///
|
||||
/// This is used to prevent incorrect transformations like:
|
||||
@@ -360,13 +432,36 @@ impl<'a> PeepholeOptimizations {
|
||||
}
|
||||
|
||||
impl<'a> Traverse<'a> for PeepholeOptimizations {
|
||||
fn enter_program(&mut self, _program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
fn enter_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
ctx.state.symbol_values.reset();
|
||||
ctx.state.proto_write_symbols.clear();
|
||||
// Any module loader (`import`, `export * from`, `export … from`) can, on a
|
||||
// cycle, evaluate a foreign module that observes a not-yet-assigned binding
|
||||
// our exports close over. So the program root starts its prelude "unsafe"
|
||||
// when the body has any loader — bailing every program-scope var inline.
|
||||
// Loaders are hoisted, so scan the whole body (an import may follow a
|
||||
// leading var); the result never changes across passes.
|
||||
let module_has_loaders = program
|
||||
.body
|
||||
.iter()
|
||||
.any(|s| s.as_module_declaration().is_some_and(|m| m.source().is_some()));
|
||||
// `enter`/`exit_function_body` are balanced, so the stack is back to its
|
||||
// single program-root entry by the next pass; reset it in place rather
|
||||
// than reallocating (matching the `reset`/`clear` above).
|
||||
*ctx.state.body_unsafe_stack.last_mut() =
|
||||
(ctx.scoping().root_scope_id(), module_has_loaders);
|
||||
// `PassDirty` is managed by the `Compressor` driver via
|
||||
// `flush_pass_dirty`, not reset per traversal.
|
||||
}
|
||||
|
||||
fn enter_function_body(&mut self, _body: &mut FunctionBody<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
ctx.state.body_unsafe_stack.push((ctx.current_scope_id(), false));
|
||||
}
|
||||
|
||||
fn exit_function_body(&mut self, _body: &mut FunctionBody<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
ctx.state.body_unsafe_stack.pop();
|
||||
}
|
||||
|
||||
fn exit_program(&mut self, _program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
// Only check class_symbols_stack in full optimization mode (not DCE mode)
|
||||
debug_assert!(ctx.state.dce || ctx.state.class_symbols_stack.is_exhausted());
|
||||
@@ -441,6 +536,12 @@ impl<'a> Traverse<'a> for PeepholeOptimizations {
|
||||
}
|
||||
Self::try_fold_expression_stmt(stmt, ctx);
|
||||
}
|
||||
|
||||
// Maintain the per-body declarative-prelude flag used by
|
||||
// `is_hoisted_var_inlineable`.
|
||||
if !Self::is_declarative_body_statement(stmt) {
|
||||
Self::mark_current_body_unsafe(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
fn exit_for_statement(&mut self, stmt: &mut ForStatement<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
@@ -475,6 +576,14 @@ impl<'a> Traverse<'a> for PeepholeOptimizations {
|
||||
ctx: &mut TraverseCtx<'a>,
|
||||
) {
|
||||
Self::init_symbol_value(decl, ctx);
|
||||
// Per-declarator update of the body-unsafe flag. Catches multi-declarator
|
||||
// statements (`var [x=call()] = '', flag = true;`, possibly produced by
|
||||
// join-vars) where an earlier declarator runs user code via a
|
||||
// destructuring default or non-literal init — the per-statement check
|
||||
// would fire too late for subsequent declarators' `init_symbol_value`.
|
||||
if !Self::is_declarative_variable_declarator(decl) {
|
||||
Self::mark_current_body_unsafe(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
|
||||
@@ -6,7 +6,7 @@ use oxc_data_structures::stack::NonEmptyStack;
|
||||
use oxc_semantic::Scoping;
|
||||
use oxc_span::SourceType;
|
||||
use oxc_str::Str;
|
||||
use oxc_syntax::symbol::SymbolId;
|
||||
use oxc_syntax::{scope::ScopeId, symbol::SymbolId};
|
||||
|
||||
use crate::{CompressOptions, symbol_value::SymbolValues};
|
||||
|
||||
@@ -66,6 +66,17 @@ pub struct MinifierState<'a> {
|
||||
/// setters that make subsequent property writes side-effectful.
|
||||
pub proto_write_symbols: FxHashSet<SymbolId>,
|
||||
|
||||
/// One frame per enclosing function body (program root at the bottom).
|
||||
/// `(body_scope, body_unsafe)`. While `body_unsafe` is false, the next
|
||||
/// `var x = <literal>;` whose declarator sits at `body_scope` is safe to
|
||||
/// inline despite hoisting. A preceding non-declarative statement sets it;
|
||||
/// the program root additionally starts unsafe when the module has any
|
||||
/// loader (`import` / `export … from` / `export * from`), since a cyclic
|
||||
/// importer could observe a not-yet-assigned binding our exports close over.
|
||||
/// Pushed by `enter_function_body`, popped by `exit_function_body`. See
|
||||
/// `init_symbol_value`.
|
||||
pub body_unsafe_stack: NonEmptyStack<(ScopeId, bool)>,
|
||||
|
||||
/// Set when a typed helper mutates the AST. Private by design: the only
|
||||
/// writers are the helpers on `MinifierTraverseCtx`; the only reader is
|
||||
/// the fixed-point loop driver via `take_mutated()`.
|
||||
@@ -98,6 +109,7 @@ impl<'a> MinifierState<'a> {
|
||||
symbol_values: SymbolValues::new(scoping.symbols_len()),
|
||||
class_symbols_stack: ClassSymbolsStack::new(),
|
||||
proto_write_symbols: FxHashSet::default(),
|
||||
body_unsafe_stack: NonEmptyStack::new((scoping.root_scope_id(), false)),
|
||||
mutated: false,
|
||||
dirty: PassDirty::new(scoping.references_len(), allocator),
|
||||
concat_scratch: String::new(),
|
||||
|
||||
@@ -1,4 +1,210 @@
|
||||
use crate::{CompressOptions, test_options, test_same_options};
|
||||
use oxc_span::SourceType;
|
||||
|
||||
use crate::{
|
||||
CompressOptions, test_options, test_options_source_type, test_same_options, test_smallest,
|
||||
};
|
||||
|
||||
// https://github.com/oxc-project/oxc/issues/13051
|
||||
#[test]
|
||||
fn readonly_var() {
|
||||
// Top-level `var` with constant initializer, only read from inside a hoisted
|
||||
// function that is called after the declaration. Safe to inline because no
|
||||
// statement before the `var` can run code that reads it.
|
||||
test_smallest(
|
||||
"var used = false; function test() { if (used) return 123; return 321; } log(test());",
|
||||
"function test() { return 321; } log(test());",
|
||||
);
|
||||
|
||||
// Multiple readonly vars in a row — every preceding statement is itself a
|
||||
// safe `var = literal` so each one is inlineable in turn.
|
||||
test_smallest(
|
||||
"var a = 1; var b = 2; function f() { return a + b; } log(f());",
|
||||
"function f() { return 3; } log(f());",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readonly_var_unsafe_preceding_call() {
|
||||
// sapphi-red's case: a preceding call could invoke `output` before `foo` is
|
||||
// assigned. The read inside `output` would see `undefined`, so inlining
|
||||
// `foo` to `true` would change observable behavior — `foo` must stay.
|
||||
test_smallest(
|
||||
"output(); var foo = true; function output() { if (!foo) log('foo'); }",
|
||||
"output(); var foo = !0; function output() { foo || log('foo'); }",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readonly_var_unsafe_preceding_read() {
|
||||
// A preceding statement reads the var before its initializer runs;
|
||||
// the read must observe `undefined`, not the constant.
|
||||
test_smallest("var y = foo; var foo = 1; log(y);", "var y = foo, foo = 1; log(y);");
|
||||
|
||||
// Canonical hoisting trap: the name is used directly before its own `var`
|
||||
// declaration, so the read sees the hoisted `undefined`. `console.log(a)`
|
||||
// must print `undefined`, never `0`. Doubly guarded — the read is in the
|
||||
// same call frame (does not cross a function boundary) and the preceding
|
||||
// call ends the declarative prelude.
|
||||
test_smallest("console.log(a); var a = 0;", "console.log(a); var a = 0;");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readonly_var_reassigned() {
|
||||
// `foo` has a write reference, so even though `var foo = 1;` is at the top,
|
||||
// inlining is unsafe.
|
||||
test_smallest("var foo = 1; foo = 2; log(foo);", "var foo = 1; foo = 2, log(foo);");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readonly_var_reassigned_cross_function_read() {
|
||||
// The read crosses a function boundary (the gap this path targets), but
|
||||
// `foo` is also reassigned. The predicate's read-loop ignores writes, so it
|
||||
// relies on the downstream `write_references_count` guard in
|
||||
// `inline_identifier_reference` to block inlining — substituting `1` would be
|
||||
// wrong once `foo = 2` runs before `f()` is called.
|
||||
test_smallest(
|
||||
"var foo = 1; foo = 2; function f() { return foo; } log(f());",
|
||||
"var foo = 1; foo = 2; function f() { return foo; } log(f());",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readonly_var_reader_declared_before_var() {
|
||||
// Known limitation: when the reading function is declared *before* the var,
|
||||
// the symbol's constant isn't recorded until `exit_variable_declarator` —
|
||||
// after `f`'s body (and its `foo` reference) was already visited in source
|
||||
// order. The in-pass design can't reach back, so this otherwise-safe case is
|
||||
// conservatively left un-inlined. Asserted to make any future improvement a
|
||||
// conscious change rather than a silent one.
|
||||
test_smallest(
|
||||
"function f() { return foo; } var foo = true; log(f());",
|
||||
"function f() { return foo; } var foo = !0; log(f());",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readonly_var_with_imports_present() {
|
||||
// A circular importer can observe ANY module-private var our exported
|
||||
// functions/classes close over, regardless of export status. As long as the
|
||||
// module has any static import, skip program-scope inlining outright.
|
||||
// Non-exported var captured by an exported function — the cyclic-closure
|
||||
// hazard Codex flagged. Must NOT inline.
|
||||
test_smallest(
|
||||
"import './b.js'; var flag = true; export function check() { return flag; }",
|
||||
"import './b.js'; var flag = !0; export function check() { return flag; }",
|
||||
);
|
||||
// Even with no export, an import anywhere disqualifies — we can't tell
|
||||
// statically whether a cycle exists.
|
||||
test_smallest(
|
||||
"import './side-effect.js'; var DEBUG = false; function log(x) { if (DEBUG) console.log(x); } log('hi');",
|
||||
"import './side-effect.js'; var DEBUG = !1; function log(x) { DEBUG && console.log(x); } log('hi');",
|
||||
);
|
||||
// Imports are hoisted, so an import appearing *after* the var in source
|
||||
// still triggers the gate — the pre-scan checks the whole body.
|
||||
test_smallest(
|
||||
"var flag = true; import './b.js'; export function check() { return flag; }",
|
||||
"var flag = !0; import './b.js'; export function check() { return flag; }",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readonly_var_with_reexports_present() {
|
||||
// `export * from` and `export { … } from` are module loaders too — they
|
||||
// evaluate foreign modules and create the same cyclic-eval hazard.
|
||||
test_smallest(
|
||||
"export * from './other.js'; var flag = true; export function check() { return flag; }",
|
||||
"export * from './other.js'; var flag = !0; export function check() { return flag; }",
|
||||
);
|
||||
test_smallest(
|
||||
"export { y } from './y.js'; var flag = true; export function check() { return flag; }",
|
||||
"export { y } from './y.js'; var flag = !0; export function check() { return flag; }",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readonly_var_through_declarative_exports() {
|
||||
// `export function …`, `export var <literal>`, `export { … }`, and
|
||||
// `export default function …` are declarative wrappers — no user code runs
|
||||
// at module init, so the body's declarative prelude continues through them
|
||||
// and a later var stays inlineable.
|
||||
test_smallest(
|
||||
"export function helper() {} export var A = 1; var b = 2; function f() { return b; } log(f());",
|
||||
"export function helper() {} export var A = 1; function f() { return 2; } log(f());",
|
||||
);
|
||||
test_smallest(
|
||||
"export default function helper() {} var b = 2; function f() { return b; } log(f());",
|
||||
"export default function helper() {} function f() { return 2; } log(f());",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readonly_var_export_default_expression_breaks_prelude() {
|
||||
// `export default <expr>` evaluates the expression at module init, which
|
||||
// can call user code. A later var must not inline.
|
||||
test_smallest(
|
||||
"export default sideEffect(); var b = 2; function f() { return b; } log(f());",
|
||||
"export default sideEffect(); var b = 2; function f() { return b; } log(f());",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readonly_var_unsafe_destructuring_default_prelude() {
|
||||
// A preceding destructuring var with a default-call evaluates the call
|
||||
// before `flag = true`. If `flag` were inlined inside `inner`, the call
|
||||
// would observe `true` instead of the required hoisted `undefined`.
|
||||
test_smallest(
|
||||
"var [x = inner()] = ''; var flag = true; function inner() { return flag; } log(x);",
|
||||
"var [x = inner()] = '', flag = !0; function inner() { return flag; } log(x);",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readonly_var_in_function_body() {
|
||||
// #2: same declarative-prelude analysis, applied to function bodies. The
|
||||
// var sits at the function's body scope and is read from a nested function,
|
||||
// which substitute_single_use_symbol can't reach.
|
||||
test_smallest(
|
||||
"function outer() { var flag = false; function inner() { return flag ? 1 : 2; } return inner(); } log(outer());",
|
||||
"function outer() { function inner() { return 2; } return inner(); } log(outer());",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readonly_var_in_function_body_unsafe_preceding_call() {
|
||||
// Same hoisting hazard as top-level: an observable call before the var
|
||||
// inside the function body could invoke a hoisted inner function that
|
||||
// reads `flag` as `undefined`. Skip.
|
||||
test_smallest(
|
||||
"function outer() { sideEffect(); var flag = true; function inner() { return flag; } return inner(); } log(outer());",
|
||||
"function outer() { sideEffect(); var flag = !0; function inner() { return flag; } return inner(); } log(outer());",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readonly_var_script_mode() {
|
||||
// Top-level `var` in script mode creates a property on the global object;
|
||||
// another script can mutate it between this line and a later function call.
|
||||
// Don't inline.
|
||||
test_options_source_type(
|
||||
"var used = false; function test() { if (used) return 123; return 321; } log(test());",
|
||||
"var used = !1; function test() { return used ? 123 : 321; } log(test());",
|
||||
SourceType::cjs().with_script(true),
|
||||
&CompressOptions::smallest(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readonly_var_after_type_declaration() {
|
||||
// Type-only declarations (`type`, `interface`) are erased and run no code,
|
||||
// so they don't end the declarative prelude — a following readonly var
|
||||
// stays inlineable.
|
||||
test_options_source_type(
|
||||
"type T = number; interface I {} var b = 2; function f() { return b; } log(f());",
|
||||
"type T = number; interface I {} function f() { return 2; } log(f());",
|
||||
SourceType::ts().with_module(true),
|
||||
&CompressOptions::smallest(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn r#const() {
|
||||
|
||||
@@ -9,7 +9,7 @@ Original | minified | minified | gzip | gzip | Iterations | Fi
|
||||
|
||||
342.15 kB | 116.97 kB | 118.14 kB | 43.18 kB | 44.37 kB | 2 | vue.js
|
||||
|
||||
544.10 kB | 71.04 kB | 72.48 kB | 25.77 kB | 26.20 kB | 2 | lodash.js
|
||||
544.10 kB | 70.96 kB | 72.48 kB | 25.77 kB | 26.20 kB | 2 | lodash.js
|
||||
|
||||
555.77 kB | 267.40 kB | 270.13 kB | 88.01 kB | 90.80 kB | 2 | d3.js
|
||||
|
||||
@@ -17,11 +17,11 @@ Original | minified | minified | gzip | gzip | Iterations | Fi
|
||||
|
||||
1.25 MB | 642.65 kB | 646.76 kB | 159.41 kB | 163.73 kB | 2 | three.js
|
||||
|
||||
2.14 MB | 711.10 kB | 724.14 kB | 160.44 kB | 181.07 kB | 2 | victory.js
|
||||
2.14 MB | 711.07 kB | 724.14 kB | 160.41 kB | 181.07 kB | 2 | victory.js
|
||||
|
||||
3.20 MB | 1.00 MB | 1.01 MB | 322.61 kB | 331.56 kB | 2 | echarts.js
|
||||
|
||||
6.69 MB | 2.22 MB | 2.31 MB | 458.45 kB | 488.28 kB | 4 | antd.js
|
||||
6.69 MB | 2.22 MB | 2.31 MB | 458.44 kB | 488.28 kB | 4 | antd.js
|
||||
|
||||
10.95 MB | 3.33 MB | 3.49 MB | 853.35 kB | 915.50 kB | 4 | typescript.js
|
||||
10.95 MB | 3.33 MB | 3.49 MB | 853.11 kB | 915.50 kB | 4 | typescript.js
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
File | File size || Sys allocs | Sys reallocs || Arena allocs | Arena reallocs
|
||||
---------------------------------------------------------------------------------------------------------------------------
|
||||
checker.ts | 2.92 MB || 165 | 51 || 152742 | 28244
|
||||
checker.ts | 2.92 MB || 166 | 52 || 152742 | 28244
|
||||
|
||||
App.tsx | 415.34 kB || 114 | 48 || 17012 | 2702
|
||||
App.tsx | 415.34 kB || 115 | 49 || 17012 | 2702
|
||||
|
||||
RadixUIAdoptionSection.jsx | 2.52 kB || 23 | 20 || 32 | 6
|
||||
RadixUIAdoptionSection.jsx | 2.52 kB || 24 | 20 || 32 | 6
|
||||
|
||||
pdf.mjs | 567.30 kB || 2613 | 248 || 47485 | 7740
|
||||
pdf.mjs | 567.30 kB || 2614 | 249 || 47485 | 7740
|
||||
|
||||
antd.js | 6.69 MB || 3083 | 104 || 337251 | 69349
|
||||
antd.js | 6.69 MB || 3084 | 106 || 337233 | 69333
|
||||
|
||||
binder.ts | 193.08 kB || 40 | 36 || 7083 | 824
|
||||
binder.ts | 193.08 kB || 41 | 36 || 7083 | 824
|
||||
|
||||
kitchen-sink.tsx | 732.90 kB || 2639 | 216 || 90554 | 15823
|
||||
kitchen-sink.tsx | 732.90 kB || 2640 | 218 || 90554 | 15823
|
||||
|
||||
Reference in new issue
Block a user