Skip to content

Static perf audit: potential O(n²) AST mutations and repeated subtree scans in crates/swc_ecma_transformer/src #11667

@kdy1

Description

@kdy1

Summary

I did a static performance audit of crates/swc_ecma_transformer/src and found several hotspots that can become expensive on large files/classes.

Most of the risk comes from:

  • Vec::insert(0, ...) / Vec::insert(i, ...) patterns inside loops (potential O(n^2) shifts)
  • Repeated remove/insert on Vec<ModuleItem> while iterating
  • Repeated full subtree scans in hot hooks
  • Linear membership checks on Vec where HashSet would be O(1)

Findings

1) Repeated front insertion in object-rest lowering (O(n^2) shifting)

In multiple hooks, we prepend statements by repeatedly calling insert(0, stmt):

  • crates/swc_ecma_transformer/src/es2018/object_rest_spread.rs:279
  • crates/swc_ecma_transformer/src/es2018/object_rest_spread.rs:309
  • crates/swc_ecma_transformer/src/es2018/object_rest_spread.rs:357
  • crates/swc_ecma_transformer/src/es2018/object_rest_spread.rs:386
  • crates/swc_ecma_transformer/src/es2018/object_rest_spread.rs:410
  • crates/swc_ecma_transformer/src/es2018/object_rest_spread.rs:444
  • crates/swc_ecma_transformer/src/es2018/object_rest_spread.rs:576
  • crates/swc_ecma_transformer/src/es2018/object_rest_spread.rs:614

If many statements are prepended to large bodies, this repeatedly shifts the tail.

Suggestion: build a new vector with capacity and append [prepended..., original...], or use a small deque-style buffering strategy before one final materialization.

2) remove(i) + multiple insert(...) in exit_module_items (O(n^2) risk)

object_rest_spread::exit_module_items mutates items in-place with middle removes/inserts while iterating:

  • remove(i): crates/swc_ecma_transformer/src/es2018/object_rest_spread.rs:478
  • multiple insert(...): crates/swc_ecma_transformer/src/es2018/object_rest_spread.rs:493, :509, :513
  • whole loop: crates/swc_ecma_transformer/src/es2018/object_rest_spread.rs:460-537

On modules with many exports, this can degenerate into repeated shifts.

Suggestion: transform via a new output vector (Vec::with_capacity) in a single pass.

3) Statement injector currently performs repeated Vec::insert in reverse pass

common/statement_injector.rs collects insertion plans, then applies them using many insert calls:

  • module items insertion loop: crates/swc_ecma_transformer/src/common/statement_injector.rs:129-155
  • statement insertion loop: crates/swc_ecma_transformer/src/common/statement_injector.rs:171-197

This is also shift-heavy and can become O(n^2) with many injected statements.

Suggestion: rebuild the vector once, emitting before + original + after per element.

4) Private-property pass stores membership sets as Vec and uses repeated .contains

ClassData tracks methods and statics as Vec<Atom>:

  • definitions: crates/swc_ecma_transformer/src/es2022/private_property_in_object.rs:132, :135
  • membership checks in hot path: :144, :149, :371, :372

These checks are linear in the number of private members.

Suggestion: switch methods and statics to FxHashSet<Atom> (or maintain both ordered vec + set only if ordering is needed).

5) Full subtree scan in enter_assign_pat for every assign pattern

private_property_in_object performs an explicit visitor walk on each AssignPat.right:

  • crates/swc_ecma_transformer/src/es2022/private_property_in_object.rs:282-290

This adds extra traversals in addition to the main transform traversal.

Suggestion: avoid per-node ad-hoc scans by carrying a lightweight context flag from the main traversal or restricting this scan to cases that already matched a fast precheck.

6) Function-name transform does full usage scan per candidate function/class

function_name calls IdentUsageFinder::find for each anonymous fn/class assignment candidate:

  • crates/swc_ecma_transformer/src/es2015/function_name.rs:111
  • crates/swc_ecma_transformer/src/es2015/function_name.rs:126

On code with many such candidates and large bodies, this introduces repeated full-body scans.

Suggestion: add cheap fast-path guards before full scan, or cache/short-circuit when the candidate name cannot appear.

7) Async-to-generator can do a second full recursive walk to replace this

In constructor-arrow cases using this, we run an additional recursive replacement pass over the arrow body:

  • call site: crates/swc_ecma_transformer/src/es2017/async_to_generator.rs:189-195
  • recursive walkers: :876-1331

This is semantically valid but can be expensive for large nested arrow bodies.

Suggestion: integrate replacement into the main traversal path when possible, or narrow second-pass scope with stronger prechecks.


Suggested follow-up

  1. Add micro-bench fixtures for:
    • many function params/object-rest cases
    • many export declarations requiring rewrite
    • large class with many private members and #x in y checks
  2. Measure before/after with wall time + allocation counts.
  3. Prioritize findings ecmascript parser #1, Clean up #2, EcmaScript lexer is inefficient #3, Transformers for ecmascript #4 first (highest expected impact).

Scope note

This report is static-analysis-only (no runtime profiling yet).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions