mirror of
https://github.com/oven-sh/bun
synced 2026-02-17 14:22:01 +00:00
### Problem The bundler's `__toESM` helper creates a new getter-wrapped proxy object every time a CJS module is imported. In a large app, a popular dependency like React can be imported 600+ times — each creating a fresh object with ~44 getter properties. This produces ~27K unnecessary `GetterSetter` objects, ~25K closures, and ~25K `JSLexicalEnvironment` scope objects at startup. Additionally, `__export` and `__exportValue` use `var`-scoped loop variables captured by setter closures, meaning all setters incorrectly reference the last iterated key (a latent bug). ### Changes 1. **`__toESM`: add WeakMap cache** — deduplicate repeated wrappings of the same CJS module. Two caches (one per `isNodeMode` value) to handle both import modes correctly. 2. **Replace closures with `.bind()`** — `() => obj[key]` becomes `__accessProp.bind(obj, key)`. BoundFunction is cheaper than Function + JSLexicalEnvironment, and frees the for-in `JSPropertyNameEnumerator` from the closure scope. 3. **Fix var-scoping bug in `__export`/`__exportValue`** — setter closures captured a shared `var name` and would all modify the last iterated key. `.bind()` eagerly captures the correct key per iteration. 4. **`__toCommonJS`: `.map()` → `for..of`** — eliminates throwaway array allocation. 5. **`__reExport`: single `getOwnPropertyNames` call** — was calling it twice when `secondTarget` was provided. ### Impact (measured on a ~23MB single-bundle app with 600+ React imports) | Metric | Before | After | Delta | |--------|--------|-------|-------| | **Total objects** | 745,985 | 664,001 | **-81,984 (-11%)** | | **Heap size** | 115 MB | 111 MB | **-4 MB** | | GetterSetter | 34,625 | 13,428 | -21,197 (-61%) | | Function | 221,302 | 197,024 | -24,278 (-11%) | | JSLexicalEnvironment | 70,101 | 44,633 | -25,468 (-36%) | | Structure | 40,254 | 39,762 | -492 |