Skip to content

Commit e9a5067

Browse files
authored
[turbopack] Promote exports and module to factory parameters for cjs (#82285)
## Optimze cjs codegen by promoting `module` and `exports` to factory parameters *tl;dr; destructing is hard, lets go shopping!* Currently we prefix factories for cjs modules with `var {m:module, e:exports} = __turbopack_context__;` This is problematic since swc is unable to eliminate these bindings when they are dead (too afraid of es5 getter properties and side effects 😭). Furthermore we are playing a game of telephone with ourselves since our caller basically is doing `factory({m:module, e: module.exports})` when they call us (this is skimming over a bunch of details, ofc). Instead just pass them directly as function parameters. One downside of this is that we still pass them to `esm` factories even though they are ignored (and simply not declared by the factories). This is not too costly since the caller has trivial access already and browsers are pretty good at handling/ignoring extra parameters (see https://v8.dev/blog/adaptor-frame for the clever way v8 optimizes this). ### Alternatives The main alternative considered was compile time replacing references to `module` and `exports` with `__turbopack_context__.(m|e)`. Unfortunately this breaks subtle edge cases of the cjs spec since it is allowed for users to rewrite `module` and `exports` with assignments (e.g. `exports = 'bad idea'`). This is generally a linter error but not unheard of and we cannot break it. The other alternative considered was enhancing `swc` to inline and dead code eliminate destructuring patterns, but from playing with the playgound it looks like swc is pretty conservative about moving/inlining variables across side effectful statements, so we would still be paying the cost of the declaration in many cases. ## Turbopack Performance Comparison: Head vs Change As expected this is a substantial improvement to commonjs evaluation. The ESM results are kind of confusing. I generally expected 'neutral' since the cost of passing a dead parameters is so small in modern vms. | Test Case | Metric | Head (ms) | Change (ms) | Speedup | Improvement | |-----------|--------|-----------|-------------|---------|-------------| | **Client CommonJS** | Load - Average | 31.61 | 24.38 | **1.30x** | **+29.6%** | | | Load - Median | 32.05 | 24.30 | **1.32x** | **+31.9%** | | | Load - P75 | 33.10 | 24.90 | **1.33x** | **+32.9%** | | | Execute - Average | 25.96 | 19.80 | **1.31x** | **+31.1%** | | | Execute - Median | 25.90 | 19.50 | **1.33x** | **+32.8%** | | | Execute - P75 | 26.50 | 20.20 | **1.31x** | **+31.2%** | | **Client ESM** | Load - Average | 22.28 | 21.94 | **1.02x** | **+1.6%** | | | Load - Median | 21.80 | 21.60 | **1.01x** | **+0.9%** | | | Load - P75 | 23.50 | 22.10 | **1.06x** | **+6.3%** | | | Execute - Average | 50.77 | 48.12 | **1.06x** | **+5.5%** | | | Execute - Median | 50.85 | 47.55 | **1.07x** | **+6.9%** | | | Execute - P75 | 51.60 | 48.30 | **1.07x** | **+6.8%** | | **Pages API CommonJS** | Load - Average | 24.18 | 23.33 | **1.04x** | **+3.7%** | | | Load - Median | 23.27 | 23.57 | 0.99x | -1.3% | | | Load - P75 | 25.29 | 23.96 | **1.06x** | **+5.5%** | | | Execute - Average | 32.23 | 24.26 | **1.33x** | **+32.8%** | | | Execute - Median | 31.16 | 24.34 | **1.28x** | **+28.0%** | | | Execute - P75 | 33.42 | 24.52 | **1.36x** | **+36.3%** | | **Pages API ESM** | Load - Average | 23.48 | 28.91 | 0.81x | -18.8% | | | Load - Median | 23.13 | 28.91 | 0.80x | -20.0% | | | Load - P75 | 25.55 | 29.57 | 0.86x | -13.6% | | | Execute - Average | 63.01 | 55.94 | **1.13x** | **+12.6%** | | | Execute - Median | 61.56 | 55.22 | **1.11x** | **+11.5%** | | | Execute - P75 | 69.56 | 56.40 | **1.23x** | **+23.3%** | **Legend:** - **Bold values** indicate performance improvements (speedups) - Regular values indicate performance regressions (slowdowns) - Speedup = Head Time ÷ Change Time (>1.0 = faster, <1.0 = slower) - P75 = 75th percentile
1 parent 74a0aa2 commit e9a5067

File tree

81 files changed

+289
-312
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

81 files changed

+289
-312
lines changed

crates/next-core/src/hmr_entry.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,6 @@ impl EcmascriptChunkItem for HmrEntryChunkItem {
192192
inner_code: code.build(),
193193
options: EcmascriptChunkItemOptions {
194194
strict: true,
195-
module: true,
196195
..Default::default()
197196
},
198197
..Default::default()

turbopack/crates/turbopack-ecmascript-runtime/js/src/browser/runtime/base/build-base.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,17 @@ function instantiateModule(
5959
}
6060

6161
const module: Module = createModuleObject(id)
62+
const exports = module.exports
6263

6364
moduleCache[id] = module
6465

6566
// NOTE(alexkirsz) This can fail when the module encounters a runtime error.
66-
const context = new (Context as any as ContextConstructor<Module>)(module)
67+
const context = new (Context as any as ContextConstructor<Module>)(
68+
module,
69+
exports
70+
)
6771
try {
68-
moduleFactory(context)
72+
moduleFactory(context, module, exports)
6973
} catch (error) {
7074
module.error = error as any
7175
throw error

turbopack/crates/turbopack-ecmascript-runtime/js/src/browser/runtime/base/dev-base.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,15 +165,20 @@ const getOrInstantiateModuleFromParent: GetOrInstantiateModuleFromParent<
165165
function DevContext(
166166
this: TurbopackDevContext,
167167
module: HotModule,
168+
exports: Exports,
168169
refresh: RefreshContext
169170
) {
170-
Context.call(this, module)
171+
Context.call(this, module, exports)
171172
this.k = refresh
172173
}
173174
DevContext.prototype = Context.prototype
174175

175176
type DevContextConstructor = {
176-
new (module: HotModule, refresh: RefreshContext): TurbopackDevContext
177+
new (
178+
module: HotModule,
179+
exports: Exports,
180+
refresh: RefreshContext
181+
): TurbopackDevContext
177182
}
178183

179184
function instantiateModule(
@@ -217,6 +222,7 @@ function instantiateModule(
217222
}
218223

219224
const module: HotModule = createModuleObject(id) as HotModule
225+
const exports = module.exports
220226
module.parents = parents
221227
module.children = []
222228
module.hot = hot
@@ -229,9 +235,10 @@ function instantiateModule(
229235
runModuleExecutionHooks(module, (refresh) => {
230236
const context = new (DevContext as any as DevContextConstructor)(
231237
module,
238+
exports,
232239
refresh
233240
)
234-
moduleFactory(context)
241+
moduleFactory(context, module, exports)
235242
})
236243
} catch (error) {
237244
module.error = error as any

turbopack/crates/turbopack-ecmascript-runtime/js/src/nodejs/runtime.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,12 +216,16 @@ function instantiateModule(
216216
}
217217

218218
const module: Module = createModuleObject(id)
219+
const exports = module.exports
219220
moduleCache[id] = module
220221

222+
const context = new (Context as any as ContextConstructor<Module>)(
223+
module,
224+
exports
225+
)
221226
// NOTE(alexkirsz) This can fail when the module encounters a runtime error.
222227
try {
223-
const context = new (Context as any as ContextConstructor<Module>)(module)
224-
moduleFactory(context)
228+
moduleFactory(context, module, exports)
225229
} catch (error) {
226230
module.error = error as any
227231
throw error

turbopack/crates/turbopack-ecmascript-runtime/js/src/shared/runtime-types.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ interface ModuleWithDirection extends Module {
115115

116116
interface TurbopackBaseContext<M> {
117117
a: AsyncModule
118-
e: Module['exports']
118+
e: Exports
119119
r: CommonJsRequire
120120
t: RuntimeRequire
121121
f: ModuleContextFactory

turbopack/crates/turbopack-ecmascript-runtime/js/src/shared/runtime-utils.ts

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,20 @@ const REEXPORTED_OBJECTS = Symbol('reexported objects')
2222
/**
2323
* Constructs the `__turbopack_context__` object for a module.
2424
*/
25-
function Context(this: TurbopackBaseContext<Module>, module: Module) {
25+
function Context(
26+
this: TurbopackBaseContext<Module>,
27+
module: Module,
28+
exports: Exports
29+
) {
2630
this.m = module
27-
this.e = module.exports
31+
// We need to store this here instead of accessing it from the module object to:
32+
// 1. Make it available to factories directly, since we rewrite `this` to
33+
// `__turbopack_context__.e` in CJS modules.
34+
// 2. Support async modules which rewrite `module.exports` to a promise, so we
35+
// can still access the original exports object from functions like
36+
// `esmExport`
37+
// Ideally we could find a new approach for async modules and drop this property altogether.
38+
this.e = exports
2839
}
2940
const contextPrototype = Context.prototype as TurbopackBaseContext<Module>
3041

@@ -145,11 +156,16 @@ function esmExport(
145156
}
146157
contextPrototype.s = esmExport
147158

148-
function ensureDynamicExports(module: Module, exports: Exports) {
149-
let reexportedObjects = module[REEXPORTED_OBJECTS]
159+
type ReexportedObjects = Record<PropertyKey, unknown>[]
160+
function ensureDynamicExports(
161+
module: Module,
162+
exports: Exports
163+
): ReexportedObjects {
164+
let reexportedObjects: ReexportedObjects | undefined =
165+
module[REEXPORTED_OBJECTS]
150166

151167
if (!reexportedObjects) {
152-
reexportedObjects = module[REEXPORTED_OBJECTS] = []
168+
module[REEXPORTED_OBJECTS] = reexportedObjects = []
153169
module.exports = module.namespaceObject = new Proxy(exports, {
154170
get(target, prop) {
155171
if (
@@ -176,6 +192,7 @@ function ensureDynamicExports(module: Module, exports: Exports) {
176192
},
177193
})
178194
}
195+
return reexportedObjects
179196
}
180197

181198
/**
@@ -186,16 +203,19 @@ function dynamicExport(
186203
object: Record<string, any>,
187204
id: ModuleId | undefined
188205
) {
189-
let module = this.m
190-
let exports = this.e
206+
let module: Module
207+
let exports: Module['exports']
191208
if (id != null) {
192209
module = getOverwrittenModule(this.c, id)
193210
exports = module.exports
211+
} else {
212+
module = this.m
213+
exports = this.e
194214
}
195-
ensureDynamicExports(module, exports)
215+
const reexportedObjects = ensureDynamicExports(module, exports)
196216

197217
if (typeof object === 'object' && object !== null) {
198-
module[REEXPORTED_OBJECTS]!.push(object)
218+
reexportedObjects.push(object)
199219
}
200220
}
201221
contextPrototype.j = dynamicExport
@@ -205,9 +225,11 @@ function exportValue(
205225
value: any,
206226
id: ModuleId | undefined
207227
) {
208-
let module = this.m
228+
let module: Module
209229
if (id != null) {
210230
module = getOverwrittenModule(this.c, id)
231+
} else {
232+
module = this.m
211233
}
212234
module.exports = value
213235
}
@@ -218,9 +240,11 @@ function exportNamespace(
218240
namespace: any,
219241
id: ModuleId | undefined
220242
) {
221-
let module = this.m
243+
let module: Module
222244
if (id != null) {
223245
module = getOverwrittenModule(this.c, id)
246+
} else {
247+
module = this.m
224248
}
225249
module.exports = module.namespaceObject = namespace
226250
}
@@ -664,7 +688,7 @@ function requireStub(_moduleId: ModuleId): never {
664688
contextPrototype.z = requireStub
665689

666690
type ContextConstructor<M> = {
667-
new (module: Module): TurbopackBaseContext<M>
691+
new (module: Module, exports: Exports): TurbopackBaseContext<M>
668692
}
669693

670694
function applyModuleFactoryName(factory: Function) {

turbopack/crates/turbopack-ecmascript/src/chunk/item.rs

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use crate::{
2121
EcmascriptModuleContent,
2222
references::async_module::{AsyncModuleOptions, OptionAsyncModuleOptions},
2323
runtime_functions::TURBOPACK_ASYNC_MODULE,
24-
utils::{FormatIter, StringifyJs},
24+
utils::StringifyJs,
2525
};
2626

2727
#[turbo_tasks::value(shared)]
@@ -66,7 +66,6 @@ impl EcmascriptChunkItemContent {
6666
strict: true,
6767
externals,
6868
async_module,
69-
stub_require: true,
7069
..Default::default()
7170
}
7271
} else {
@@ -78,8 +77,7 @@ impl EcmascriptChunkItemContent {
7877
strict,
7978
externals,
8079
// These things are not available in ESM
81-
module: true,
82-
exports: true,
80+
module_and_exports: true,
8381
..Default::default()
8482
}
8583
},
@@ -90,37 +88,27 @@ impl EcmascriptChunkItemContent {
9088

9189
#[turbo_tasks::function]
9290
pub async fn module_factory(&self) -> Result<Vc<Code>> {
93-
let mut args = Vec::new();
94-
if self.options.module {
95-
args.push("m: module");
96-
}
97-
if self.options.exports {
98-
args.push("e: exports");
99-
}
100-
10191
let mut code = CodeBuilder::default();
10292
for additional_id in self.additional_ids.iter().try_join().await? {
10393
writeln!(code, "{}, ", StringifyJs(&*additional_id))?;
10494
}
105-
code += "((__turbopack_context__) => {\n";
95+
if self.options.module_and_exports {
96+
code += "((__turbopack_context__, module, exports) => {\n";
97+
} else {
98+
code += "((__turbopack_context__) => {\n";
99+
}
106100
if self.options.strict {
107101
code += "\"use strict\";\n\n";
108102
} else {
109103
code += "\n";
110104
}
111-
if !args.is_empty() {
112-
let args = FormatIter(|| args.iter().copied().intersperse(", "));
113-
writeln!(code, "var {{ {args} }} = __turbopack_context__;")?;
114-
}
115105

116106
if self.options.async_module.is_some() {
117107
writeln!(
118108
code,
119109
"return {TURBOPACK_ASYNC_MODULE}(async (__turbopack_handle_async_dependencies__, \
120110
__turbopack_async_result__) => {{ try {{\n"
121111
)?;
122-
} else if !args.is_empty() {
123-
code += "{\n";
124112
}
125113

126114
let source_map = if let Some(rewrite_source_path) = &self.rewrite_source_path {
@@ -138,8 +126,6 @@ impl EcmascriptChunkItemContent {
138126
}}, {});",
139127
opts.has_top_level_await
140128
)?;
141-
} else if !args.is_empty() {
142-
code += "}";
143129
}
144130

145131
code += "})";
@@ -154,15 +140,9 @@ impl EcmascriptChunkItemContent {
154140
pub struct EcmascriptChunkItemOptions {
155141
/// Whether this chunk item should be in "use strict" mode.
156142
pub strict: bool,
157-
/// Whether this chunk item's module factory should include a `module`
158-
/// argument.
159-
pub module: bool,
160-
/// Whether this chunk item's module factory should include an `exports`
161-
/// argument.
162-
pub exports: bool,
163-
/// Whether this chunk item's module factory should include an argument for a throwing require
164-
/// stub (for ESM)
165-
pub stub_require: bool,
143+
/// Whether this chunk item's module factory should include a `module` and
144+
/// `exports` argument.
145+
pub module_and_exports: bool,
166146
/// Whether this chunk item's module factory should include a
167147
/// `__turbopack_external_require__` argument.
168148
pub externals: bool,

turbopack/crates/turbopack-ecmascript/src/tree_shake/chunk_item.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,6 @@ impl EcmascriptChunkItem for SideEffectsModuleChunkItem {
169169
rewrite_source_path: None,
170170
options: EcmascriptChunkItemOptions {
171171
strict: true,
172-
exports: true,
173172
async_module: if has_top_level_await {
174173
Some(AsyncModuleOptions {
175174
has_top_level_await: true,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
exports = () => 'hello'
2+
if (typeof exports === 'object') throw 'oh no'
3+
module.exports = 1234
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import moduleUniversal from './module-universal.js'
22
import moduleReassign from './module-reassign.js'
3+
import exportsReassign from './exports-reassign.js'
34

45
it('should use the CommonJS variant of universal module definitions', () => {
56
expect(moduleUniversal()).toBe('other-dep 1234')
67
})
78

89
it('should not replace typeof exports for non-free variables', () => {
9-
expect(moduleReassign).toBe(1234)
10+
expect(exportsReassign).toBe(1234)
11+
})
12+
13+
it('should not replace typeof module for non-free variables', () => {
14+
expect(moduleReassign.foo).toBe(1234)
1015
})

0 commit comments

Comments
 (0)