Repository: vercel/next.js
Pull request: Turbopack: less eager manifest generation

Before:

<img width="2560" height="1224" alt="Bildschirmfoto 2025-09-12 um 18 47 11" src="https://github.com/user-attachments/assets/c40d2be6-f570-40b4-8a82-7b181f2de073" />


After:
<img width="2560" height="1323" alt="Bildschirmfoto 2025-09-12 um 19 15 35" src="https://github.com/user-attachments/assets/b58826c4-88f0-4efb-b70a-fd384785af21" />

Not that drastic of a difference, because this is still a problem:

<img width="972" height="826" alt="Bildschirmfoto 2025-09-12 um 19 15 41" src="https://github.com/user-attachments/assets/5bd63a13-27ba-468c-a0a4-e66639bc47a3" />


Technically, when calling `::reference()`, there is no need to compute the path, but `ChunkData` does that on initialization.


`NEXT_TURBOPACK_TRACING=turbo-tasks pnpm next build --turbo test/production/pages-dir/production/fixture`

Closes PACK-5503

Changed files:
filename: crates/next-api/src/app.rs
patch: @@ -1362,21 +1362,15 @@ impl AppEndpoint {
             }
 
             let build_manifest = BuildManifest {
+                output_path: node_root.join(&format!(
+                    "server/app{manifest_path_prefix}/build-manifest.json",
+                ))?,
+                client_relative_path: client_relative_path.clone(),
+                pages: Default::default(),
                 root_main_files: client_shared_chunks,
                 polyfill_files: vec![polyfill_output_asset],
-                ..Default::default()
             };
-            let build_manifest_output = build_manifest
-                .build_output(
-                    node_root.join(&format!(
-                        "server/app{manifest_path_prefix}/build-manifest.json",
-                    ))?,
-                    client_relative_path.clone(),
-                )
-                .await?
-                .to_resolved()
-                .await?;
-            server_assets.insert(build_manifest_output);
+            server_assets.insert(ResolvedVc::upcast(build_manifest.resolved_cell()));
         }
 
         if runtime == NextRuntime::Edge {

filename: crates/next-api/src/pages.rs
patch: @@ -12,8 +12,8 @@ use next_core::{
     next_dynamic::NextDynamicTransition,
     next_edge::route_regex::get_named_middleware_regex,
     next_manifests::{
-        BuildManifest, EdgeFunctionDefinition, MiddlewareMatcher, MiddlewaresManifestV2,
-        PagesManifest, Regions,
+        BuildManifest, ClientBuildManifest, EdgeFunctionDefinition, MiddlewareMatcher,
+        MiddlewaresManifestV2, PagesManifest, Regions,
     },
     next_pages::create_page_ssr_entry_module,
     next_server::{
@@ -1235,51 +1235,50 @@ impl PageEndpoint {
             fxindexmap![] // Empty pages when no user pages should be created
         };
 
+        let manifest_path_prefix = get_asset_prefix_from_pathname(&self.pathname);
         let build_manifest = BuildManifest {
+            output_path: node_root.join(&format!(
+                "server/pages{manifest_path_prefix}/build-manifest.json",
+            ))?,
+            client_relative_path,
             pages,
-            ..Default::default()
+            polyfill_files: Default::default(),
+            root_main_files: Default::default(),
         };
-        let manifest_path_prefix = get_asset_prefix_from_pathname(&self.pathname);
-        build_manifest
-            .build_output(
-                node_root.join(&format!(
-                    "server/pages{manifest_path_prefix}/build-manifest.json",
-                ))?,
-                client_relative_path,
-            )
-            .await
+        Ok(Vc::upcast(build_manifest.cell()))
     }
 
     #[turbo_tasks::function]
     async fn client_build_manifest(
-        self: Vc<Self>,
+        &self,
         page_loader: ResolvedVc<Box<dyn OutputAsset>>,
     ) -> Result<Vc<Box<dyn OutputAsset>>> {
-        let this = self.await?;
-        let node_root = this.pages_project.project().node_root().await?;
-        let client_relative_path = this.pages_project.project().client_relative_path().await?;
+        let node_root = self.pages_project.project().node_root().await?;
+        let client_relative_path = self
+            .pages_project
+            .project()
+            .client_relative_path()
+            .owned()
+            .await?;
 
         // Check if we should include pages in the manifest
-        let pages_structure = this.pages_structure.await?;
-        let client_build_manifest = if pages_structure.should_create_pages_entries {
-            let page_loader_path = client_relative_path
-                .get_relative_path_to(&*page_loader.path().await?)
-                .context("failed to resolve client-relative path to page loader")?;
-            fxindexmap!(this.pathname.clone() => vec![page_loader_path])
+        let pages_structure = self.pages_structure.await?;
+        let pages = if pages_structure.should_create_pages_entries {
+            fxindexmap!(self.pathname.clone() => page_loader)
         } else {
-            fxindexmap![] // Empty manifest when no user pages should be created
+            fxindexmap![] // Empty pages when no user pages should be created
         };
 
-        let manifest_path_prefix = get_asset_prefix_from_pathname(&this.pathname);
-        Ok(Vc::upcast(VirtualOutputAsset::new_with_references(
-            node_root.join(&format!(
+        let manifest_path_prefix = get_asset_prefix_from_pathname(&self.pathname);
+        let client_build_manifest = ClientBuildManifest {
+            output_path: node_root.join(&format!(
                 "server/pages{manifest_path_prefix}/client-build-manifest.json",
             ))?,
-            AssetContent::file(
-                File::from(serde_json::to_string_pretty(&client_build_manifest)?).into(),
-            ),
-            Vc::cell(vec![page_loader]),
-        )))
+            client_relative_path,
+            pages,
+        };
+
+        Ok(Vc::upcast(client_build_manifest.cell()))
     }
 
     #[turbo_tasks::function]

filename: crates/next-core/src/next_manifests/mod.rs
patch: @@ -12,33 +12,56 @@ use turbo_tasks::{
 };
 use turbo_tasks_fs::{File, FileSystemPath};
 use turbopack_core::{
-    asset::AssetContent,
+    asset::{Asset, AssetContent},
     output::{OutputAsset, OutputAssets},
-    virtual_output::VirtualOutputAsset,
 };
 
-use crate::next_config::{CrossOriginConfig, Rewrites, RouteHas};
+use crate::next_config::{CrossOriginConfig, RouteHas};
 
 #[derive(Serialize, Default, Debug)]
 pub struct PagesManifest {
     #[serde(flatten)]
     pub pages: FxIndexMap<RcStr, RcStr>,
 }
 
-#[derive(Debug, Default)]
+#[derive(Debug)]
+#[turbo_tasks::value(shared)]
 pub struct BuildManifest {
+    pub output_path: FileSystemPath,
+    pub client_relative_path: FileSystemPath,
+
     pub polyfill_files: Vec<ResolvedVc<Box<dyn OutputAsset>>>,
     pub root_main_files: Vec<ResolvedVc<Box<dyn OutputAsset>>>,
     pub pages: FxIndexMap<RcStr, ResolvedVc<OutputAssets>>,
 }
 
-impl BuildManifest {
-    pub async fn build_output(
-        self,
-        output_path: FileSystemPath,
-        client_relative_path: FileSystemPath,
-    ) -> Result<Vc<Box<dyn OutputAsset>>> {
-        let client_relative_path_ref = client_relative_path.clone();
+#[turbo_tasks::value_impl]
+impl OutputAsset for BuildManifest {
+    #[turbo_tasks::function]
+    async fn path(&self) -> Vc<FileSystemPath> {
+        self.output_path.clone().cell()
+    }
+
+    #[turbo_tasks::function]
+    async fn references(&self) -> Result<Vc<OutputAssets>> {
+        let chunks: Vec<ReadRef<OutputAssets>> = self.pages.values().try_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())
+            .chain(self.polyfill_files.iter().copied())
+            .collect();
+
+        Ok(Vc::cell(references))
+    }
+}
+
+#[turbo_tasks::value_impl]
+impl Asset for BuildManifest {
+    #[turbo_tasks::function]
+    async fn content(&self) -> Result<Vc<AssetContent>> {
+        let client_relative_path = &self.client_relative_path;
 
         #[derive(Serialize, Default, Debug)]
         #[serde(rename_all = "camelCase")]
@@ -55,33 +78,23 @@ impl BuildManifest {
         let pages: Vec<(RcStr, Vec<RcStr>)> = self
             .pages
             .iter()
-            .map(|(k, chunks)| {
-                let client_relative_path_ref = client_relative_path_ref.clone();
-
-                async move {
-                    Ok((
-                        k.clone(),
-                        chunks
-                            .await?
-                            .iter()
-                            .copied()
-                            .map(|chunk| {
-                                let client_relative_path_ref = client_relative_path_ref.clone();
-                                async move {
-                                    let chunk_path = chunk.path().await?;
-                                    Ok(client_relative_path_ref
-                                        .get_path_to(&chunk_path)
-                                        .context(
-                                            "client chunk entry path must be inside the client \
-                                             root",
-                                        )?
-                                        .into())
-                                }
-                            })
-                            .try_join()
-                            .await?,
-                    ))
-                }
+            .map(|(k, chunks)| async move {
+                Ok((
+                    k.clone(),
+                    chunks
+                        .await?
+                        .iter()
+                        .copied()
+                        .map(async |chunk| {
+                            let chunk_path = chunk.path().await?;
+                            Ok(client_relative_path
+                                .get_path_to(&chunk_path)
+                                .context("client chunk entry path must be inside the client root")?
+                                .into())
+                        })
+                        .try_join()
+                        .await?,
+                ))
             })
             .try_join()
             .await?;
@@ -90,16 +103,12 @@ impl BuildManifest {
             .polyfill_files
             .iter()
             .copied()
-            .map(|chunk| {
-                let client_relative_path_ref = client_relative_path_ref.clone();
-
-                async move {
-                    let chunk_path = chunk.path().await?;
-                    Ok(client_relative_path_ref
-                        .get_path_to(&chunk_path)
-                        .context("failed to resolve client-relative path to polyfill")?
-                        .into())
-                }
+            .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 polyfill")?
+                    .into())
             })
             .try_join()
             .await?;
@@ -108,16 +117,12 @@ impl BuildManifest {
             .root_main_files
             .iter()
             .copied()
-            .map(|chunk| {
-                let client_relative_path_ref = client_relative_path_ref.clone();
-
-                async move {
-                    let chunk_path = chunk.path().await?;
-                    Ok(client_relative_path_ref
-                        .get_path_to(&chunk_path)
-                        .context("failed to resolve client-relative path to root_main_file")?
-                        .into())
-                }
+            .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())
             })
             .try_join()
             .await?;
@@ -129,20 +134,63 @@ impl BuildManifest {
             ..Default::default()
         };
 
-        let chunks: Vec<ReadRef<OutputAssets>> = self.pages.values().try_join().await?;
+        Ok(AssetContent::file(
+            File::from(serde_json::to_string_pretty(&manifest)?).into(),
+        ))
+    }
+}
 
-        let references = chunks
+#[derive(Debug)]
+#[turbo_tasks::value(shared)]
+pub struct ClientBuildManifest {
+    pub output_path: FileSystemPath,
+    pub client_relative_path: FileSystemPath,
+
+    pub pages: FxIndexMap<RcStr, ResolvedVc<Box<dyn OutputAsset>>>,
+}
+
+#[turbo_tasks::value_impl]
+impl OutputAsset for ClientBuildManifest {
+    #[turbo_tasks::function]
+    async fn path(&self) -> Vc<FileSystemPath> {
+        self.output_path.clone().cell()
+    }
+
+    #[turbo_tasks::function]
+    async fn references(&self) -> Result<Vc<OutputAssets>> {
+        let chunks: Vec<ResolvedVc<Box<dyn OutputAsset>>> = self.pages.values().copied().collect();
+        Ok(Vc::cell(chunks))
+    }
+}
+
+#[turbo_tasks::value_impl]
+impl Asset for ClientBuildManifest {
+    #[turbo_tasks::function]
+    async fn content(&self) -> Result<Vc<AssetContent>> {
+        let client_relative_path = &self.client_relative_path;
+
+        let manifest: FxIndexMap<RcStr, Vec<RcStr>> = self
+            .pages
+            .iter()
+            .map(async |(k, chunk)| {
+                Ok((
+                    k.clone(),
+                    vec![
+                        client_relative_path
+                            .get_path_to(&*chunk.path().await?)
+                            .context("client chunk entry path must be inside the client root")?
+                            .into(),
+                    ],
+                ))
+            })
+            .try_join()
+            .await?
             .into_iter()
-            .flat_map(|c| c.into_iter().copied()) // once again, rustc struggles here
-            .chain(self.root_main_files.iter().copied())
-            .chain(self.polyfill_files.iter().copied())
             .collect();
 
-        Ok(Vc::upcast(VirtualOutputAsset::new_with_references(
-            output_path,
-            AssetContent::file(File::from(serde_json::to_string_pretty(&manifest)?).into()),
-            Vc::cell(references),
-        )))
+        Ok(AssetContent::file(
+            File::from(serde_json::to_string_pretty(&manifest)?).into(),
+        ))
     }
 }
 
@@ -439,19 +487,6 @@ pub struct FontManifestEntry {
     pub content: RcStr,
 }
 
-// TODO(alexkirsz) Unify with the one for dev.
-#[derive(Serialize, Debug)]
-#[serde(rename_all = "camelCase")]
-pub struct ClientBuildManifest<'a> {
-    #[serde(rename = "__rewrites")]
-    pub rewrites: &'a Rewrites,
-
-    pub sorted_pages: &'a [RcStr],
-
-    #[serde(flatten)]
-    pub pages: FxIndexMap<RcStr, Vec<&'a str>>,
-}
-
 #[cfg(test)]
 mod tests {
     use turbo_rcstr::rcstr;