Repository: vercel/next.js
Pull request: Turbopack: lazier manifests, part deux

Lazily compute the contents of the client reference and the font manifests, this moves the computation to `emit`​ (= writing out all chunks in parallel) as opposed to `Endpoint::output`​ (= where we are determining the list of chunks to emit).

<img width="1891" height="910" alt="Bildschirmfoto 2025-09-24 um 18 30 13" src="https://github.com/user-attachments/assets/3d3dbd83-4125-4bb5-bb90-e38c536cc44e" />

Closes PACK-5567

Changed files:
filename: crates/next-api/src/app.rs
patch: @@ -23,9 +23,9 @@ use next_core::{
     next_dynamic::NextDynamicTransition,
     next_edge::route_regex::get_named_middleware_regex,
     next_manifests::{
-        AppPathsManifest, BuildManifest, ClientReferenceManifest, EdgeFunctionDefinition,
-        MiddlewareMatcher, MiddlewaresManifestV2, PagesManifest, Regions,
-        client_reference_manifest::ClientReferenceManifestOptions,
+        AppPathsManifest, BuildManifest, EdgeFunctionDefinition, MiddlewareMatcher,
+        MiddlewaresManifestV2, PagesManifest, Regions,
+        client_reference_manifest::ClientReferenceManifest,
     },
     next_server::{
         ServerContextType, get_server_module_options_context, get_server_resolve_options_context,
@@ -74,7 +74,7 @@ use turbopack_ecmascript::resolve::cjs_resolve;
 
 use crate::{
     dynamic_imports::{NextDynamicChunkAvailability, collect_next_dynamic_chunks},
-    font::create_font_manifest,
+    font::FontManifest,
     loadable_manifest::create_react_loadable_manifest,
     module_graph::get_global_information_for_endpoint,
     nft_json::NftJsonAsset,
@@ -1239,18 +1239,10 @@ impl AppEndpoint {
             )
             .await?;
 
-            let mut client_shared_chunks = vec![];
-            for &chunk in client_shared_chunk_group.assets.await? {
-                client_assets.insert(chunk);
+            client_assets.extend(client_shared_chunk_group.referenced_assets.await?);
 
-                let chunk_path = chunk.path().await?;
-                if chunk_path.has_extension(".js") {
-                    client_shared_chunks.push(chunk);
-                }
-            }
-            for &chunk in client_shared_chunk_group.referenced_assets.await? {
-                client_assets.insert(chunk);
-            }
+            let client_shared_chunks = client_shared_chunk_group.assets.owned().await?;
+            client_assets.extend(client_shared_chunks.iter().copied());
 
             (
                 client_shared_chunk_group.availability_info,
@@ -1462,8 +1454,8 @@ impl AppEndpoint {
         let mut client_reference_manifest = None;
 
         if emit_rsc_manifests {
-            let entry_manifest =
-                ClientReferenceManifest::build_output(ClientReferenceManifestOptions {
+            let entry_manifest = ResolvedVc::upcast(
+                ClientReferenceManifest {
                     node_root: node_root.clone(),
                     client_relative_path: client_relative_path.clone(),
                     entry_name: app_entry.original_name.clone(),
@@ -1475,27 +1467,29 @@ impl AppEndpoint {
                     next_config: project.next_config().to_resolved().await?,
                     runtime,
                     mode: *project.next_mode().await?,
-                })
-                .to_resolved()
-                .await?;
+                }
+                .resolved_cell(),
+            );
             server_assets.insert(entry_manifest);
             if runtime == NextRuntime::Edge {
                 middleware_assets.insert(entry_manifest);
             }
             client_reference_manifest = Some(entry_manifest);
         }
         if emit_manifests == EmitManifests::Full {
-            let next_font_manifest_output = create_font_manifest(
-                project.client_root().owned().await?,
-                node_root.clone(),
-                this.app_project.app_dir().owned().await?,
-                &app_entry.original_name,
-                &app_entry.original_name,
-                &app_entry.original_name,
-                *client_assets,
-                true,
-            )
-            .await?;
+            let next_font_manifest_output = ResolvedVc::upcast(
+                FontManifest {
+                    client_root: project.client_root().owned().await?,
+                    node_root: node_root.clone(),
+                    dir: this.app_project.app_dir().owned().await?,
+                    original_name: app_entry.original_name.clone(),
+                    manifest_path_prefix: app_entry.original_name.clone(),
+                    pathname: app_entry.original_name.clone(),
+                    client_assets,
+                    app_dir: true,
+                }
+                .resolved_cell(),
+            );
             server_assets.insert(next_font_manifest_output);
         }
 

filename: crates/next-api/src/font.rs
patch: @@ -4,79 +4,93 @@ use turbo_rcstr::RcStr;
 use turbo_tasks::{ResolvedVc, Vc};
 use turbo_tasks_fs::{File, FileSystemPath};
 use turbopack_core::{
-    asset::AssetContent,
+    asset::{Asset, AssetContent},
     output::{OutputAsset, OutputAssets},
-    virtual_output::VirtualOutputAsset,
 };
 
 use crate::paths::get_font_paths_from_root;
 
-pub(crate) async fn create_font_manifest(
-    client_root: FileSystemPath,
-    node_root: FileSystemPath,
-    dir: FileSystemPath,
-    original_name: &str,
-    manifest_path_prefix: &str,
-    pathname: &str,
-    client_assets: Vc<OutputAssets>,
-    app_dir: bool,
-) -> Result<ResolvedVc<Box<dyn OutputAsset>>> {
-    let all_client_output_assets = all_assets_from_entries(client_assets).await?;
+#[turbo_tasks::value(shared)]
+pub struct FontManifest {
+    pub client_root: FileSystemPath,
+    pub node_root: FileSystemPath,
+    pub dir: FileSystemPath,
+    pub original_name: RcStr,
+    pub manifest_path_prefix: RcStr,
+    pub pathname: RcStr,
+    pub client_assets: ResolvedVc<OutputAssets>,
+    pub app_dir: bool,
+}
+
+#[turbo_tasks::value_impl]
+impl OutputAsset for FontManifest {
+    #[turbo_tasks::function]
+    async fn path(&self) -> Result<Vc<FileSystemPath>> {
+        let manifest_path_prefix = &self.manifest_path_prefix;
+        Ok(self
+            .node_root
+            .join(&format!(
+                "server/{}{manifest_path_prefix}/next-font-manifest.json",
+                if self.app_dir { "app" } else { "pages" }
+            ))?
+            .cell())
+    }
+}
 
-    // `_next` gets added again later, so we "strip" it here via
-    // `get_font_paths_from_root`.
-    let font_paths: Vec<String> = get_font_paths_from_root(&client_root, &all_client_output_assets)
-        .await?
-        .iter()
-        .filter_map(|p| p.split("_next/").last().map(|f| f.to_string()))
-        .collect();
+#[turbo_tasks::value_impl]
+impl Asset for FontManifest {
+    #[turbo_tasks::function]
+    async fn content(&self) -> Result<Vc<AssetContent>> {
+        let FontManifest {
+            client_root,
+            dir,
+            original_name,
+            pathname,
+            client_assets,
+            app_dir,
+            ..
+        } = self;
+        let all_client_output_assets = all_assets_from_entries(**client_assets).await?;
 
-    let path = if app_dir {
-        node_root.join(&format!(
-            "server/app{manifest_path_prefix}/next-font-manifest.json",
-        ))?
-    } else {
-        node_root.join(&format!(
-            "server/pages{manifest_path_prefix}/next-font-manifest.json",
-        ))?
-    };
+        // `_next` gets added again later, so we "strip" it here via
+        // `get_font_paths_from_root`.
+        let font_paths: Vec<String> =
+            get_font_paths_from_root(client_root, &all_client_output_assets)
+                .await?
+                .iter()
+                .filter_map(|p| p.split("_next/").last().map(|f| f.to_string()))
+                .collect();
 
-    let has_fonts = !font_paths.is_empty();
-    let using_size_adjust = font_paths.iter().any(|path| path.contains("-s"));
+        let has_fonts = !font_paths.is_empty();
+        let using_size_adjust = font_paths.iter().any(|path| path.contains("-s"));
 
-    let font_paths = font_paths
-        .into_iter()
-        .filter(|path| path.contains(".p."))
-        .map(RcStr::from)
-        .collect::<Vec<_>>();
+        let font_paths = font_paths
+            .into_iter()
+            .filter(|path| path.contains(".p."))
+            .map(RcStr::from)
+            .collect::<Vec<_>>();
 
-    let next_font_manifest = if !has_fonts {
-        Default::default()
-    } else if app_dir {
-        let dir_str = dir.value_to_string().await?;
-        let page_path = format!("{dir_str}{original_name}").into();
+        let next_font_manifest = if !has_fonts {
+            Default::default()
+        } else if *app_dir {
+            let dir_str = dir.value_to_string().await?;
+            let page_path = format!("{dir_str}{original_name}").into();
 
-        NextFontManifest {
-            app: [(page_path, font_paths)].into_iter().collect(),
-            app_using_size_adjust: using_size_adjust,
-            ..Default::default()
-        }
-    } else {
-        NextFontManifest {
-            pages: [(pathname.into(), font_paths)].into_iter().collect(),
-            pages_using_size_adjust: using_size_adjust,
-            ..Default::default()
-        }
-    };
+            NextFontManifest {
+                app: [(page_path, font_paths)].into_iter().collect(),
+                app_using_size_adjust: using_size_adjust,
+                ..Default::default()
+            }
+        } else {
+            NextFontManifest {
+                pages: [(pathname.clone(), font_paths)].into_iter().collect(),
+                pages_using_size_adjust: using_size_adjust,
+                ..Default::default()
+            }
+        };
 
-    Ok(ResolvedVc::upcast(
-        VirtualOutputAsset::new(
-            path,
-            AssetContent::file(
-                File::from(serde_json::to_string_pretty(&next_font_manifest)?).into(),
-            ),
-        )
-        .to_resolved()
-        .await?,
-    ))
+        Ok(AssetContent::file(
+            File::from(serde_json::to_string_pretty(&next_font_manifest)?).into(),
+        ))
+    }
 }

filename: crates/next-api/src/pages.rs
patch: @@ -69,7 +69,7 @@ use crate::{
     dynamic_imports::{
         DynamicImportedChunks, NextDynamicChunkAvailability, collect_next_dynamic_chunks,
     },
-    font::create_font_manifest,
+    font::FontManifest,
     loadable_manifest::create_react_loadable_manifest,
     module_graph::get_global_information_for_endpoint,
     nft_json::NftJsonAsset,
@@ -1350,17 +1350,19 @@ impl PageEndpoint {
         let node_root = this.pages_project.project().node_root().owned().await?;
 
         if emit_manifests == EmitManifests::Full {
-            let next_font_manifest_output = create_font_manifest(
-                this.pages_project.project().client_root().owned().await?,
-                node_root.clone(),
-                this.pages_project.pages_dir().owned().await?,
-                &this.original_name,
-                &manifest_path_prefix,
-                &this.pathname,
-                *client_assets,
-                false,
-            )
-            .await?;
+            let next_font_manifest_output = ResolvedVc::upcast(
+                FontManifest {
+                    client_root: this.pages_project.project().client_root().owned().await?,
+                    node_root: node_root.clone(),
+                    dir: this.pages_project.pages_dir().owned().await?,
+                    original_name: this.original_name.clone(),
+                    manifest_path_prefix: manifest_path_prefix.clone().into(),
+                    pathname: this.pathname.clone(),
+                    client_assets,
+                    app_dir: false,
+                }
+                .resolved_cell(),
+            );
             server_assets.push(next_font_manifest_output);
         }
 

filename: crates/next-core/src/next_manifests/client_reference_manifest.rs
patch: @@ -3,12 +3,11 @@ use either::Either;
 use indoc::formatdoc;
 use itertools::Itertools;
 use rustc_hash::FxHashMap;
-use serde::{Deserialize, Serialize};
+use serde::Serialize;
 use tracing::Instrument;
 use turbo_rcstr::{RcStr, rcstr};
 use turbo_tasks::{
-    FxIndexSet, ResolvedVc, TaskInput, TryFlatJoinIterExt, TryJoinIterExt, ValueToString, Vc,
-    trace::TraceRawVcs,
+    FxIndexMap, FxIndexSet, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, ValueToString, Vc,
 };
 use turbo_tasks_fs::{File, FileSystemPath};
 use turbopack_core::{
@@ -17,23 +16,84 @@ use turbopack_core::{
         ChunkGroupResult, ChunkingContext, ModuleChunkItemIdExt, ModuleId as TurbopackModuleId,
     },
     module_graph::async_module_info::AsyncModulesInfo,
-    output::{OutputAsset, OutputAssetsWithReferenced},
-    virtual_output::VirtualOutputAsset,
+    output::{OutputAsset, OutputAssets, OutputAssetsWithReferenced},
 };
 use turbopack_ecmascript::utils::StringifyJs;
 
-use super::{ClientReferenceManifest, CssResource, ManifestNode, ManifestNodeEntry, ModuleId};
 use crate::{
     mode::NextMode,
     next_app::ClientReferencesChunks,
     next_client_reference::{ClientReferenceGraphResult, ClientReferenceType},
-    next_config::NextConfig,
-    next_manifests::encode_uri_component::encode_uri_component,
+    next_config::{CrossOriginConfig, NextConfig},
+    next_manifests::{ModuleId, encode_uri_component::encode_uri_component},
     util::NextRuntime,
 };
 
-#[derive(TaskInput, Clone, Hash, Debug, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs)]
-pub struct ClientReferenceManifestOptions {
+#[derive(Serialize, Default, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct SerializedClientReferenceManifest {
+    pub module_loading: ModuleLoading,
+    /// Mapping of module path and export name to client module ID and required
+    /// client chunks.
+    pub client_modules: ManifestNode,
+    /// Mapping of client module ID to corresponding SSR module ID and required
+    /// SSR chunks.
+    pub ssr_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
+    /// Same as `ssr_module_mapping`, but for Edge SSR.
+    #[serde(rename = "edgeSSRModuleMapping")]
+    pub edge_ssr_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
+    /// Mapping of client module ID to corresponding RSC module ID and required
+    /// RSC chunks.
+    pub rsc_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
+    /// Same as `rsc_module_mapping`, but for Edge RSC.
+    #[serde(rename = "edgeRscModuleMapping")]
+    pub edge_rsc_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
+    /// Mapping of server component path to required CSS client chunks.
+    #[serde(rename = "entryCSSFiles")]
+    pub entry_css_files: FxIndexMap<RcStr, FxIndexSet<CssResource>>,
+    /// Mapping of server component path to required JS client chunks.
+    #[serde(rename = "entryJSFiles")]
+    pub entry_js_files: FxIndexMap<RcStr, FxIndexSet<RcStr>>,
+}
+
+#[derive(Serialize, Debug, Clone, Eq, Hash, PartialEq)]
+pub struct CssResource {
+    pub path: RcStr,
+    pub inlined: bool,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub content: Option<RcStr>,
+}
+
+#[derive(Serialize, Default, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct ModuleLoading {
+    pub prefix: RcStr,
+    pub cross_origin: Option<CrossOriginConfig>,
+}
+
+#[derive(Serialize, Default, Debug, Clone)]
+#[serde(rename_all = "camelCase")]
+pub struct ManifestNode {
+    /// Mapping of export name to manifest node entry.
+    #[serde(flatten)]
+    pub module_exports: FxIndexMap<RcStr, ManifestNodeEntry>,
+}
+
+#[derive(Serialize, Debug, Clone)]
+#[serde(rename_all = "camelCase")]
+pub struct ManifestNodeEntry {
+    /// Turbopack module ID.
+    pub id: ModuleId,
+    /// Export name.
+    pub name: RcStr,
+    /// Chunks for the module. JS and CSS.
+    pub chunks: Vec<RcStr>,
+    // TODO(WEB-434)
+    pub r#async: bool,
+}
+
+#[turbo_tasks::value(shared)]
+pub struct ClientReferenceManifest {
     pub node_root: FileSystemPath,
     pub client_relative_path: FileSystemPath,
     pub entry_name: RcStr,
@@ -48,380 +108,402 @@ pub struct ClientReferenceManifestOptions {
 }
 
 #[turbo_tasks::value_impl]
-impl ClientReferenceManifest {
+impl OutputAsset for ClientReferenceManifest {
     #[turbo_tasks::function]
-    pub async fn build_output(
-        options: ClientReferenceManifestOptions,
-    ) -> Result<Vc<Box<dyn OutputAsset>>> {
-        let ClientReferenceManifestOptions {
-            node_root,
-            client_relative_path,
-            entry_name,
-            client_references,
-            client_references_chunks,
-            client_chunking_context,
-            ssr_chunking_context,
-            async_module_info,
-            next_config,
-            runtime,
-            mode,
-        } = options;
-        let span = tracing::info_span!(
-            "ClientReferenceManifest build output",
-            entry_name = display(&entry_name)
-        );
-        async move {
-            let mut entry_manifest: ClientReferenceManifest = Default::default();
-            let mut references = FxIndexSet::default();
-            let chunk_suffix_path = next_config.chunk_suffix_path().owned().await?;
-            let prefix_path = next_config
-                .computed_asset_prefix()
-                .owned()
-                .await?
-                .unwrap_or_default();
-            let suffix_path = chunk_suffix_path.unwrap_or_default();
+    async fn path(&self) -> Result<Vc<FileSystemPath>> {
+        let normalized_manifest_entry = self.entry_name.replace("%5F", "_");
+        Ok(self
+            .node_root
+            .join(&format!(
+                "server/app{normalized_manifest_entry}_client-reference-manifest.js",
+            ))?
+            .cell())
+    }
 
-            // TODO: Add `suffix` to the manifest for React to use.
-            // entry_manifest.module_loading.prefix = prefix_path;
+    #[turbo_tasks::function]
+    async fn references(self: Vc<Self>) -> Result<Vc<OutputAssets>> {
+        Ok(*build_manifest(self).await?.references)
+    }
+}
 
-            entry_manifest.module_loading.cross_origin = next_config
-                .await?
-                .cross_origin
-                .as_ref()
-                .map(|p| p.to_owned());
-            let ClientReferencesChunks {
-                client_component_client_chunks,
-                layout_segment_client_chunks,
-                client_component_ssr_chunks,
-            } = &*client_references_chunks.await?;
-            let client_relative_path = client_relative_path.clone();
-            let node_root_ref = node_root.clone();
-
-            let client_references_ecmascript = client_references
-                .await?
-                .client_references
-                .iter()
-                .map(async |r| {
-                    Ok(match r.ty() {
-                        ClientReferenceType::EcmascriptClientReference(r) => Some((r, r.await?)),
-                        ClientReferenceType::CssClientReference(_) => None,
+#[turbo_tasks::value_impl]
+impl Asset for ClientReferenceManifest {
+    #[turbo_tasks::function]
+    async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
+        Ok(*build_manifest(self).await?.content)
+    }
+}
+
+#[turbo_tasks::value(shared)]
+struct ClientReferenceManifestResult {
+    content: ResolvedVc<AssetContent>,
+    references: ResolvedVc<OutputAssets>,
+}
+
+#[turbo_tasks::function]
+async fn build_manifest(
+    manifest: Vc<ClientReferenceManifest>,
+) -> Result<Vc<ClientReferenceManifestResult>> {
+    let ClientReferenceManifest {
+        node_root,
+        client_relative_path,
+        entry_name,
+        client_references,
+        client_references_chunks,
+        client_chunking_context,
+        ssr_chunking_context,
+        async_module_info,
+        next_config,
+        runtime,
+        mode,
+    } = &*manifest.await?;
+    let span = tracing::info_span!(
+        "build ClientReferenceManifest",
+        entry_name = display(&entry_name)
+    );
+    async move {
+        let mut entry_manifest: SerializedClientReferenceManifest = Default::default();
+        let mut references = FxIndexSet::default();
+        let chunk_suffix_path = next_config.chunk_suffix_path().owned().await?;
+        let prefix_path = next_config
+            .computed_asset_prefix()
+            .owned()
+            .await?
+            .unwrap_or_default();
+        let suffix_path = chunk_suffix_path.unwrap_or_default();
+
+        // TODO: Add `suffix` to the manifest for React to use.
+        // entry_manifest.module_loading.prefix = prefix_path;
+
+        entry_manifest.module_loading.cross_origin = next_config
+            .await?
+            .cross_origin
+            .as_ref()
+            .map(|p| p.to_owned());
+        let ClientReferencesChunks {
+            client_component_client_chunks,
+            layout_segment_client_chunks,
+            client_component_ssr_chunks,
+        } = &*client_references_chunks.await?;
+        let client_relative_path = client_relative_path.clone();
+        let node_root_ref = node_root.clone();
+
+        let client_references_ecmascript = client_references
+            .await?
+            .client_references
+            .iter()
+            .map(async |r| {
+                Ok(match r.ty() {
+                    ClientReferenceType::EcmascriptClientReference(r) => Some((r, r.await?)),
+                    ClientReferenceType::CssClientReference(_) => None,
+                })
+            })
+            .try_flat_join()
+            .await?;
+
+        let async_modules = async_module_info
+            .is_async_multiple(Vc::cell(
+                client_references_ecmascript
+                    .iter()
+                    .flat_map(|(r, r_val)| {
+                        [
+                            ResolvedVc::upcast(*r),
+                            ResolvedVc::upcast(r_val.client_module),
+                            ResolvedVc::upcast(r_val.ssr_module),
+                        ]
+                    })
+                    .collect(),
+            ))
+            .await?;
+
+        async fn cached_chunk_paths(
+            cache: &mut FxHashMap<ResolvedVc<Box<dyn OutputAsset>>, FileSystemPath>,
+            chunks: impl Iterator<Item = ResolvedVc<Box<dyn OutputAsset>>>,
+        ) -> Result<impl Iterator<Item = (ResolvedVc<Box<dyn OutputAsset>>, FileSystemPath)>>
+        {
+            let results = chunks
+                .into_iter()
+                .map(|chunk| (chunk, cache.get(&chunk).cloned()))
+                .map(async |(chunk, path)| {
+                    Ok(if let Some(path) = path {
+                        (chunk, Either::Left(path))
+                    } else {
+                        (chunk, Either::Right(chunk.path().owned().await?))
                     })
                 })
-                .try_flat_join()
+                .try_join()
                 .await?;
 
-            let async_modules = async_module_info
-                .is_async_multiple(Vc::cell(
-                    client_references_ecmascript
-                        .iter()
-                        .flat_map(|(r, r_val)| {
-                            [
-                                ResolvedVc::upcast(*r),
-                                ResolvedVc::upcast(r_val.client_module),
-                                ResolvedVc::upcast(r_val.ssr_module),
-                            ]
-                        })
-                        .collect(),
-                ))
+            for (chunk, path) in &results {
+                if let Either::Right(path) = path {
+                    cache.insert(*chunk, path.clone());
+                }
+            }
+            Ok(results.into_iter().map(|(chunk, path)| match path {
+                Either::Left(path) => (chunk, path),
+                Either::Right(path) => (chunk, path),
+            }))
+        }
+        let mut client_chunk_path_cache: FxHashMap<
+            ResolvedVc<Box<dyn OutputAsset>>,
+            FileSystemPath,
+        > = FxHashMap::default();
+        let mut ssr_chunk_path_cache: FxHashMap<ResolvedVc<Box<dyn OutputAsset>>, FileSystemPath> =
+            FxHashMap::default();
+
+        for (client_reference_module, client_reference_module_ref) in client_references_ecmascript {
+            let app_client_reference_ty =
+                ClientReferenceType::EcmascriptClientReference(client_reference_module);
+
+            let server_path = client_reference_module_ref.server_ident.to_string().await?;
+            let client_module = client_reference_module_ref.client_module;
+            let client_chunk_item_id = client_module
+                .chunk_item_id(**client_chunking_context)
                 .await?;
 
-            async fn cached_chunk_paths(
-                cache: &mut FxHashMap<ResolvedVc<Box<dyn OutputAsset>>, FileSystemPath>,
-                chunks: impl Iterator<Item = ResolvedVc<Box<dyn OutputAsset>>>,
-            ) -> Result<impl Iterator<Item = (ResolvedVc<Box<dyn OutputAsset>>, FileSystemPath)>>
+            let (client_chunks_paths, client_is_async) = if let Some(ChunkGroupResult {
+                assets: client_chunks,
+                referenced_assets: client_referenced_assets,
+                availability_info: _,
+            }) =
+                client_component_client_chunks.get(&app_client_reference_ty)
             {
-                let results = chunks
-                    .into_iter()
-                    .map(|chunk| (chunk, cache.get(&chunk).cloned()))
-                    .map(async |(chunk, path)| {
-                        Ok(if let Some(path) = path {
-                            (chunk, Either::Left(path))
-                        } else {
-                            (chunk, Either::Right(chunk.path().owned().await?))
-                        })
+                let client_chunks = client_chunks.await?;
+                let client_referenced_assets = client_referenced_assets.await?;
+                references.extend(client_chunks.iter());
+                references.extend(client_referenced_assets.iter());
+
+                let client_chunks_paths =
+                    cached_chunk_paths(&mut client_chunk_path_cache, client_chunks.iter().copied())
+                        .await?;
+
+                let chunk_paths = client_chunks_paths
+                    .filter_map(|(_, chunk_path)| {
+                        client_relative_path
+                            .get_path_to(&chunk_path)
+                            .map(ToString::to_string)
                     })
-                    .try_join()
-                    .await?;
+                    // It's possible that a chunk also emits CSS files, that will
+                    // be handled separately.
+                    .filter(|path| path.ends_with(".js"))
+                    .map(|path| {
+                        format!(
+                            "{}{}{}",
+                            prefix_path,
+                            path.split('/').map(encode_uri_component).format("/"),
+                            suffix_path
+                        )
+                    })
+                    .map(RcStr::from)
+                    .collect::<Vec<_>>();
 
-                for (chunk, path) in &results {
-                    if let Either::Right(path) = path {
-                        cache.insert(*chunk, path.clone());
-                    }
-                }
-                Ok(results.into_iter().map(|(chunk, path)| match path {
-                    Either::Left(path) => (chunk, path),
-                    Either::Right(path) => (chunk, path),
-                }))
-            }
-            let mut client_chunk_path_cache: FxHashMap<
-                ResolvedVc<Box<dyn OutputAsset>>,
-                FileSystemPath,
-            > = FxHashMap::default();
-            let mut ssr_chunk_path_cache: FxHashMap<
-                ResolvedVc<Box<dyn OutputAsset>>,
-                FileSystemPath,
-            > = FxHashMap::default();
-
-            for (client_reference_module, client_reference_module_ref) in
-                client_references_ecmascript
-            {
-                let app_client_reference_ty =
-                    ClientReferenceType::EcmascriptClientReference(client_reference_module);
+                let is_async = async_modules.contains(&ResolvedVc::upcast(client_module));
+
+                (chunk_paths, is_async)
+            } else {
+                (Vec::new(), false)
+            };
 
-                let server_path = client_reference_module_ref.server_ident.to_string().await?;
-                let client_module = client_reference_module_ref.client_module;
-                let client_chunk_item_id = client_module
-                    .chunk_item_id(*client_chunking_context)
+            if let Some(ssr_chunking_context) = *ssr_chunking_context {
+                let ssr_module = client_reference_module_ref.ssr_module;
+                let ssr_chunk_item_id = ssr_module.chunk_item_id(*ssr_chunking_context).await?;
+
+                let rsc_chunk_item_id = client_reference_module
+                    .chunk_item_id(*ssr_chunking_context)
                     .await?;
 
-                let (client_chunks_paths, client_is_async) = if let Some(ChunkGroupResult {
-                    assets: client_chunks,
-                    referenced_assets: client_referenced_assets,
+                let (ssr_chunks_paths, ssr_is_async) = if *runtime == NextRuntime::Edge {
+                    // the chunks get added to the middleware-manifest.json instead
+                    // of this file because the
+                    // edge runtime doesn't support dynamically
+                    // loading chunks.
+                    (Vec::new(), false)
+                } else if let Some(ChunkGroupResult {
+                    assets: ssr_chunks,
+                    referenced_assets: ssr_referenced_assets,
                     availability_info: _,
-                }) =
-                    client_component_client_chunks.get(&app_client_reference_ty)
+                }) = client_component_ssr_chunks.get(&app_client_reference_ty)
                 {
-                    let client_chunks = client_chunks.await?;
-                    let client_referenced_assets = client_referenced_assets.await?;
-                    references.extend(client_chunks.iter());
-                    references.extend(client_referenced_assets.iter());
-
-                    let client_chunks_paths = cached_chunk_paths(
-                        &mut client_chunk_path_cache,
-                        client_chunks.iter().copied(),
-                    )
-                    .await?;
-
-                    let chunk_paths = client_chunks_paths
+                    let ssr_chunks = ssr_chunks.await?;
+                    let ssr_referenced_assets = ssr_referenced_assets.await?;
+                    references.extend(ssr_chunks.iter());
+                    references.extend(ssr_referenced_assets.iter());
+
+                    let ssr_chunks_paths =
+                        cached_chunk_paths(&mut ssr_chunk_path_cache, ssr_chunks.iter().copied())
+                            .await?;
+                    let chunk_paths = ssr_chunks_paths
                         .filter_map(|(_, chunk_path)| {
-                            client_relative_path
+                            node_root_ref
                                 .get_path_to(&chunk_path)
                                 .map(ToString::to_string)
                         })
-                        // It's possible that a chunk also emits CSS files, that will
-                        // be handled separately.
-                        .filter(|path| path.ends_with(".js"))
-                        .map(|path| {
-                            format!(
-                                "{}{}{}",
-                                prefix_path,
-                                path.split('/').map(encode_uri_component).format("/"),
-                                suffix_path
-                            )
-                        })
                         .map(RcStr::from)
                         .collect::<Vec<_>>();
 
-                    let is_async = async_modules.contains(&ResolvedVc::upcast(client_module));
+                    let is_async = async_modules.contains(&ResolvedVc::upcast(ssr_module));
 
                     (chunk_paths, is_async)
                 } else {
                     (Vec::new(), false)
                 };
 
-                if let Some(ssr_chunking_context) = ssr_chunking_context {
-                    let ssr_module = client_reference_module_ref.ssr_module;
-                    let ssr_chunk_item_id = ssr_module.chunk_item_id(*ssr_chunking_context).await?;
-
-                    let rsc_chunk_item_id = client_reference_module
-                        .chunk_item_id(*ssr_chunking_context)
-                        .await?;
-
-                    let (ssr_chunks_paths, ssr_is_async) = if runtime == NextRuntime::Edge {
-                        // the chunks get added to the middleware-manifest.json instead
-                        // of this file because the
-                        // edge runtime doesn't support dynamically
-                        // loading chunks.
-                        (Vec::new(), false)
-                    } else if let Some(ChunkGroupResult {
-                        assets: ssr_chunks,
-                        referenced_assets: ssr_referenced_assets,
-                        availability_info: _,
-                    }) = client_component_ssr_chunks.get(&app_client_reference_ty)
-                    {
-                        let ssr_chunks = ssr_chunks.await?;
-                        let ssr_referenced_assets = ssr_referenced_assets.await?;
-                        references.extend(ssr_chunks.iter());
-                        references.extend(ssr_referenced_assets.iter());
-
-                        let ssr_chunks_paths = cached_chunk_paths(
-                            &mut ssr_chunk_path_cache,
-                            ssr_chunks.iter().copied(),
-                        )
-                        .await?;
-                        let chunk_paths = ssr_chunks_paths
-                            .filter_map(|(_, chunk_path)| {
-                                node_root_ref
-                                    .get_path_to(&chunk_path)
-                                    .map(ToString::to_string)
-                            })
-                            .map(RcStr::from)
-                            .collect::<Vec<_>>();
-
-                        let is_async = async_modules.contains(&ResolvedVc::upcast(ssr_module));
-
-                        (chunk_paths, is_async)
-                    } else {
-                        (Vec::new(), false)
-                    };
+                let rsc_is_async = if *runtime == NextRuntime::Edge {
+                    false
+                } else {
+                    async_modules.contains(&ResolvedVc::upcast(client_reference_module))
+                };
 
-                    let rsc_is_async = if runtime == NextRuntime::Edge {
-                        false
-                    } else {
-                        async_modules.contains(&ResolvedVc::upcast(client_reference_module))
-                    };
-
-                    entry_manifest.client_modules.module_exports.insert(
-                        get_client_reference_module_key(&server_path, "*"),
-                        ManifestNodeEntry {
-                            name: rcstr!("*"),
-                            id: (&*client_chunk_item_id).into(),
-                            chunks: client_chunks_paths,
-                            // This should of course be client_is_async, but SSR can become
-                            // async due to ESM externals, and
-                            // the ssr_manifest_node is currently ignored
-                            // by React.
-                            r#async: client_is_async || ssr_is_async,
-                        },
-                    );
-
-                    let mut ssr_manifest_node = ManifestNode::default();
-                    ssr_manifest_node.module_exports.insert(
-                        rcstr!("*"),
-                        ManifestNodeEntry {
-                            name: rcstr!("*"),
-                            id: (&*ssr_chunk_item_id).into(),
-                            chunks: ssr_chunks_paths,
-                            // See above
-                            r#async: client_is_async || ssr_is_async,
-                        },
-                    );
-
-                    let mut rsc_manifest_node = ManifestNode::default();
-                    rsc_manifest_node.module_exports.insert(
-                        rcstr!("*"),
-                        ManifestNodeEntry {
-                            name: rcstr!("*"),
-                            id: (&*rsc_chunk_item_id).into(),
-                            chunks: vec![],
-                            r#async: rsc_is_async,
-                        },
-                    );
-
-                    match runtime {
-                        NextRuntime::NodeJs => {
-                            entry_manifest
-                                .ssr_module_mapping
-                                .insert((&*client_chunk_item_id).into(), ssr_manifest_node);
-                            entry_manifest
-                                .rsc_module_mapping
-                                .insert((&*client_chunk_item_id).into(), rsc_manifest_node);
-                        }
-                        NextRuntime::Edge => {
-                            entry_manifest
-                                .edge_ssr_module_mapping
-                                .insert((&*client_chunk_item_id).into(), ssr_manifest_node);
-                            entry_manifest
-                                .edge_rsc_module_mapping
-                                .insert((&*client_chunk_item_id).into(), rsc_manifest_node);
-                        }
+                entry_manifest.client_modules.module_exports.insert(
+                    get_client_reference_module_key(&server_path, "*"),
+                    ManifestNodeEntry {
+                        name: rcstr!("*"),
+                        id: (&*client_chunk_item_id).into(),
+                        chunks: client_chunks_paths,
+                        // This should of course be client_is_async, but SSR can become
+                        // async due to ESM externals, and
+                        // the ssr_manifest_node is currently ignored
+                        // by React.
+                        r#async: client_is_async || ssr_is_async,
+                    },
+                );
+
+                let mut ssr_manifest_node = ManifestNode::default();
+                ssr_manifest_node.module_exports.insert(
+                    rcstr!("*"),
+                    ManifestNodeEntry {
+                        name: rcstr!("*"),
+                        id: (&*ssr_chunk_item_id).into(),
+                        chunks: ssr_chunks_paths,
+                        // See above
+                        r#async: client_is_async || ssr_is_async,
+                    },
+                );
+
+                let mut rsc_manifest_node = ManifestNode::default();
+                rsc_manifest_node.module_exports.insert(
+                    rcstr!("*"),
+                    ManifestNodeEntry {
+                        name: rcstr!("*"),
+                        id: (&*rsc_chunk_item_id).into(),
+                        chunks: vec![],
+                        r#async: rsc_is_async,
+                    },
+                );
+
+                match runtime {
+                    NextRuntime::NodeJs => {
+                        entry_manifest
+                            .ssr_module_mapping
+                            .insert((&*client_chunk_item_id).into(), ssr_manifest_node);
+                        entry_manifest
+                            .rsc_module_mapping
+                            .insert((&*client_chunk_item_id).into(), rsc_manifest_node);
+                    }
+                    NextRuntime::Edge => {
+                        entry_manifest
+                            .edge_ssr_module_mapping
+                            .insert((&*client_chunk_item_id).into(), ssr_manifest_node);
+                        entry_manifest
+                            .edge_rsc_module_mapping
+                            .insert((&*client_chunk_item_id).into(), rsc_manifest_node);
                     }
                 }
             }
+        }
 
-            // per layout segment chunks need to be emitted into the manifest too
-            for (
-                server_component,
-                OutputAssetsWithReferenced {
-                    assets: client_chunks,
-                    referenced_assets: _,
-                },
-            ) in layout_segment_client_chunks.iter()
-            {
-                let server_component_name = server_component
-                    .server_path()
-                    .await?
-                    .with_extension("")
-                    .value_to_string()
-                    .owned()
+        // per layout segment chunks need to be emitted into the manifest too
+        for (
+            server_component,
+            OutputAssetsWithReferenced {
+                assets: client_chunks,
+                referenced_assets: _,
+            },
+        ) in layout_segment_client_chunks.iter()
+        {
+            let server_component_name = server_component
+                .server_path()
+                .await?
+                .with_extension("")
+                .value_to_string()
+                .owned()
+                .await?;
+            let entry_js_files = entry_manifest
+                .entry_js_files
+                .entry(server_component_name.clone())
+                .or_default();
+            let entry_css_files = entry_manifest
+                .entry_css_files
+                .entry(server_component_name)
+                .or_default();
+
+            let client_chunks = &client_chunks.await?;
+            let client_chunks_with_path =
+                cached_chunk_paths(&mut client_chunk_path_cache, client_chunks.iter().copied())
                     .await?;
-                let entry_js_files = entry_manifest
-                    .entry_js_files
-                    .entry(server_component_name.clone())
-                    .or_default();
-                let entry_css_files = entry_manifest
-                    .entry_css_files
-                    .entry(server_component_name)
-                    .or_default();
-
-                let client_chunks = &client_chunks.await?;
-                let client_chunks_with_path =
-                    cached_chunk_paths(&mut client_chunk_path_cache, client_chunks.iter().copied())
-                        .await?;
-                // Inlining breaks HMR so it is always disabled in dev.
-                let inlined_css = next_config.await?.experimental.inline_css.unwrap_or(false)
-                    && mode.is_production();
-
-                for (chunk, chunk_path) in client_chunks_with_path {
-                    if let Some(path) = client_relative_path.get_path_to(&chunk_path) {
-                        // The entry CSS files and entry JS files don't have prefix and suffix
-                        // applied because it is added by Next.js during rendering.
-                        let path = path.into();
-                        if chunk_path.has_extension(".css") {
-                            let content = if inlined_css {
-                                Some(
-                                    if let Some(content_file) =
-                                        chunk.content().file_content().await?.as_content()
-                                    {
-                                        content_file.content().to_str()?.into()
-                                    } else {
-                                        RcStr::default()
-                                    },
-                                )
-                            } else {
-                                None
-                            };
-                            entry_css_files.insert(CssResource {
-                                path,
-                                inlined: inlined_css,
-                                content,
-                            });
+            // Inlining breaks HMR so it is always disabled in dev.
+            let inlined_css =
+                next_config.await?.experimental.inline_css.unwrap_or(false) && mode.is_production();
+
+            for (chunk, chunk_path) in client_chunks_with_path {
+                if let Some(path) = client_relative_path.get_path_to(&chunk_path) {
+                    // The entry CSS files and entry JS files don't have prefix and suffix
+                    // applied because it is added by Next.js during rendering.
+                    let path = path.into();
+                    if chunk_path.has_extension(".css") {
+                        let content = if inlined_css {
+                            Some(
+                                if let Some(content_file) =
+                                    chunk.content().file_content().await?.as_content()
+                                {
+                                    content_file.content().to_str()?.into()
+                                } else {
+                                    RcStr::default()
+                                },
+                            )
                         } else {
-                            entry_js_files.insert(path);
-                        }
+                            None
+                        };
+                        entry_css_files.insert(CssResource {
+                            path,
+                            inlined: inlined_css,
+                            content,
+                        });
+                    } else {
+                        entry_js_files.insert(path);
                     }
                 }
             }
+        }
 
-            let client_reference_manifest_json = serde_json::to_string(&entry_manifest).unwrap();
-
-            // We put normalized path for the each entry key and the manifest output path,
-            // to conform next.js's load client reference manifest behavior:
-            // https://github.com/vercel/next.js/blob/2f9d718695e4c90be13c3bf0f3647643533071bf/packages/next/src/server/load-components.ts#L162-L164
-            // note this only applies to the manifests, assets are placed to the original
-            // path still (same as webpack does)
-            let normalized_manifest_entry = entry_name.replace("%5F", "_");
-            Ok(Vc::upcast(VirtualOutputAsset::new_with_references(
-                node_root.join(&format!(
-                    "server/app{normalized_manifest_entry}_client-reference-manifest.js",
-                ))?,
-                AssetContent::file(
-                    File::from(formatdoc! {
-                        r#"
+        let client_reference_manifest_json = serde_json::to_string(&entry_manifest).unwrap();
+
+        // We put normalized path for the each entry key and the manifest output path,
+        // to conform next.js's load client reference manifest behavior:
+        // https://github.com/vercel/next.js/blob/2f9d718695e4c90be13c3bf0f3647643533071bf/packages/next/src/server/load-components.ts#L162-L164
+        // note this only applies to the manifests, assets are placed to the original
+        // path still (same as webpack does)
+        let normalized_manifest_entry = entry_name.replace("%5F", "_");
+        Ok(ClientReferenceManifestResult {
+            content: AssetContent::file(
+                File::from(formatdoc! {
+                    r#"
                         globalThis.__RSC_MANIFEST = globalThis.__RSC_MANIFEST || {{}};
                         globalThis.__RSC_MANIFEST[{entry_name}] = {manifest}
                     "#,
-                        entry_name = StringifyJs(&normalized_manifest_entry),
-                        manifest = &client_reference_manifest_json
-                    })
-                    .into(),
-                ),
-                Vc::cell(references.into_iter().collect()),
-            )))
+                    entry_name = StringifyJs(&normalized_manifest_entry),
+                    manifest = &client_reference_manifest_json
+                })
+                .into(),
+            )
+            .to_resolved()
+            .await?,
+            references: ResolvedVc::cell(references.into_iter().collect()),
         }
-        .instrument(span)
-        .await
+        .cell())
     }
+    .instrument(span)
+    .await
 }
 
 impl From<&TurbopackModuleId> for ModuleId {

filename: crates/next-core/src/next_manifests/mod.rs
patch: @@ -7,16 +7,16 @@ use anyhow::{Context, Result};
 use serde::{Deserialize, Serialize};
 use turbo_rcstr::RcStr;
 use turbo_tasks::{
-    FxIndexMap, FxIndexSet, NonLocalValue, ReadRef, ResolvedVc, TaskInput, TryJoinIterExt, Vc,
-    trace::TraceRawVcs,
+    FxIndexMap, NonLocalValue, ReadRef, ResolvedVc, TaskInput, TryFlatJoinIterExt, TryJoinIterExt,
+    Vc, trace::TraceRawVcs,
 };
 use turbo_tasks_fs::{File, FileSystemPath};
 use turbopack_core::{
     asset::{Asset, AssetContent},
     output::{OutputAsset, OutputAssets},
 };
 
-use crate::next_config::{CrossOriginConfig, RouteHas};
+use crate::next_config::RouteHas;
 
 #[derive(Serialize, Default, Debug)]
 pub struct PagesManifest {
@@ -46,10 +46,18 @@ impl OutputAsset for BuildManifest {
     async fn references(&self) -> Result<Vc<OutputAssets>> {
         let chunks: Vec<ReadRef<OutputAssets>> = self.pages.values().try_join().await?;
 
+        let root_main_files = self
+            .root_main_files
+            .iter()
+            .map(async |c| Ok(c.path().await?.has_extension(".js").then_some(*c)))
+            .try_flat_join()
+            .await?;
+
         let references = chunks
             .into_iter()
-            .flat_map(|c| c.into_iter().copied()) // once again, rustc struggles here
-            .chain(self.root_main_files.iter().copied())
+            .flatten()
+            .copied()
+            .chain(root_main_files.into_iter())
             .chain(self.polyfill_files.iter().copied())
             .collect();
 
@@ -78,7 +86,7 @@ impl Asset for BuildManifest {
         let pages: Vec<(RcStr, Vec<RcStr>)> = self
             .pages
             .iter()
-            .map(|(k, chunks)| async move {
+            .map(async |(k, chunks)| {
                 Ok((
                     k.clone(),
                     chunks
@@ -116,15 +124,20 @@ impl Asset for BuildManifest {
         let root_main_files: Vec<RcStr> = self
             .root_main_files
             .iter()
-            .copied()
             .map(async |chunk| {
                 let chunk_path = chunk.path().await?;
-                Ok(client_relative_path
-                    .get_path_to(&chunk_path)
-                    .context("failed to resolve client-relative path to root_main_file")?
-                    .into())
+                if !chunk_path.has_extension(".js") {
+                    Ok(None)
+                } else {
+                    Ok(Some(
+                        client_relative_path
+                            .get_path_to(&chunk_path)
+                            .context("failed to resolve client-relative path to root_main_file")?
+                            .into(),
+                    ))
+                }
             })
-            .try_join()
+            .try_flat_join()
             .await?;
 
         let manifest = SerializedBuildManifest {
@@ -405,69 +418,6 @@ pub enum ActionLayer {
     ActionBrowser,
 }
 
-#[derive(Serialize, Default, Debug)]
-#[serde(rename_all = "camelCase")]
-pub struct ClientReferenceManifest {
-    pub module_loading: ModuleLoading,
-    /// Mapping of module path and export name to client module ID and required
-    /// client chunks.
-    pub client_modules: ManifestNode,
-    /// Mapping of client module ID to corresponding SSR module ID and required
-    /// SSR chunks.
-    pub ssr_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
-    /// Same as `ssr_module_mapping`, but for Edge SSR.
-    #[serde(rename = "edgeSSRModuleMapping")]
-    pub edge_ssr_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
-    /// Mapping of client module ID to corresponding RSC module ID and required
-    /// RSC chunks.
-    pub rsc_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
-    /// Same as `rsc_module_mapping`, but for Edge RSC.
-    #[serde(rename = "edgeRscModuleMapping")]
-    pub edge_rsc_module_mapping: FxIndexMap<ModuleId, ManifestNode>,
-    /// Mapping of server component path to required CSS client chunks.
-    #[serde(rename = "entryCSSFiles")]
-    pub entry_css_files: FxIndexMap<RcStr, FxIndexSet<CssResource>>,
-    /// Mapping of server component path to required JS client chunks.
-    #[serde(rename = "entryJSFiles")]
-    pub entry_js_files: FxIndexMap<RcStr, FxIndexSet<RcStr>>,
-}
-
-#[derive(Serialize, Debug, Clone, Eq, Hash, PartialEq)]
-pub struct CssResource {
-    pub path: RcStr,
-    pub inlined: bool,
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub content: Option<RcStr>,
-}
-
-#[derive(Serialize, Default, Debug)]
-#[serde(rename_all = "camelCase")]
-pub struct ModuleLoading {
-    pub prefix: RcStr,
-    pub cross_origin: Option<CrossOriginConfig>,
-}
-
-#[derive(Serialize, Default, Debug, Clone)]
-#[serde(rename_all = "camelCase")]
-pub struct ManifestNode {
-    /// Mapping of export name to manifest node entry.
-    #[serde(flatten)]
-    pub module_exports: FxIndexMap<RcStr, ManifestNodeEntry>,
-}
-
-#[derive(Serialize, Debug, Clone)]
-#[serde(rename_all = "camelCase")]
-pub struct ManifestNodeEntry {
-    /// Turbopack module ID.
-    pub id: ModuleId,
-    /// Export name.
-    pub name: RcStr,
-    /// Chunks for the module. JS and CSS.
-    pub chunks: Vec<RcStr>,
-    // TODO(WEB-434)
-    pub r#async: bool,
-}
-
 #[derive(Serialize, Debug, Eq, PartialEq, Hash, Clone)]
 #[serde(rename_all = "camelCase")]
 #[serde(untagged)]