Repository: vercel/next.js
Pull request: Turbopack: don't block on parsing traced modules

Closes PACK-4162 

We were (unnecessarily) calling `is_self_async()` even for the traced externals (the directly referenced ones, not transitively). 
This created a bottleneck during the global information computation:
<img width="2560" height="1315" alt="Bildschirmfoto 2025-08-13 um 09 50 19" src="https://github.com/user-attachments/assets/4f5d1fa6-3d65-40b2-875e-8a4a579b234b" />

`compute_async_module_info_single` calls `is_self_async` for every single module, regardless of whether the computed value is actually used or not (the traced modules have `ChunkingType::Traced`, so they'd never cause any importers to be async anyway). So manually optimize this case by overriding `is_self_async` for traced externals. This defers the parsing to emitting which is much more parallel:

<img width="2560" height="1315" alt="Bildschirmfoto 2025-08-13 um 09 51 23" src="https://github.com/user-attachments/assets/820c7157-7fe0-4253-b02f-1e0d2a8c7e7d" />


Closes PACK-5243

Changed files:
filename: turbopack/crates/turbopack-ecmascript/src/references/external_module.rs
patch: @@ -255,7 +255,18 @@ impl Module for CachedExternalModule {
                     .affecting_sources
                     .iter()
                     .map(|s| Vc::upcast::<Box<dyn Module>>(RawModule::new(**s)))
-                    .chain(external_result.primary_modules_raw_iter().map(|rvc| *rvc))
+                    .chain(
+                        external_result
+                            .primary_modules_raw_iter()
+                            // These modules aren't bundled but still need to be part of the module
+                            // graph for chunking. `compute_async_module_info` computes
+                            // `is_self_async` for every module, but at least for traced modules,
+                            // that value is never used as `ChunkingType::Traced.is_inherit_async()
+                            // == false`. Optimize this case by using `ModuleWithoutSelfAsync` to
+                            // short circuit that computation and thus defer parsing traced modules
+                            // to emitting to not block all of chunking on this.
+                            .map(|m| Vc::upcast(ModuleWithoutSelfAsync::new(*m))),
+                    )
                     .map(|s| {
                         Vc::upcast::<Box<dyn ModuleReference>>(TracedModuleReference::new(s))
                             .to_resolved()
@@ -402,3 +413,41 @@ impl EcmascriptChunkItem for CachedExternalModuleChunkItem {
         )
     }
 }
+
+/// A wrapper "passthrough" module type that always returns `false` for `is_self_async`. Be careful
+/// when using it, as it may hide async dependencies.
+#[turbo_tasks::value]
+pub struct ModuleWithoutSelfAsync {
+    module: ResolvedVc<Box<dyn Module>>,
+}
+
+#[turbo_tasks::value_impl]
+impl ModuleWithoutSelfAsync {
+    #[turbo_tasks::function]
+    pub fn new(module: ResolvedVc<Box<dyn Module>>) -> Vc<Self> {
+        Self::cell(ModuleWithoutSelfAsync { module })
+    }
+}
+
+#[turbo_tasks::value_impl]
+impl Asset for ModuleWithoutSelfAsync {
+    #[turbo_tasks::function]
+    fn content(&self) -> Vc<AssetContent> {
+        self.module.content()
+    }
+}
+
+#[turbo_tasks::value_impl]
+impl Module for ModuleWithoutSelfAsync {
+    #[turbo_tasks::function]
+    fn ident(&self) -> Vc<AssetIdent> {
+        self.module.ident()
+    }
+
+    #[turbo_tasks::function]
+    fn references(&self) -> Vc<ModuleReferences> {
+        self.module.references()
+    }
+
+    // Don't override and use default is_self_async that always returns false
+}