From e20fb2a7da088f63b49ba95af4018e86d5a42ce5 Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Fri, 28 Nov 2025 19:15:00 -0500 Subject: [PATCH] WIP: perf: Don't inject the entire nextConfig in edge templates --- .../next-core/src/next_app/app_page_entry.rs | 11 ++- .../next-core/src/next_app/app_route_entry.rs | 7 +- crates/next-core/src/next_config.rs | 71 ++++--------------- crates/next-core/src/next_pages/page_entry.rs | 9 ++- packages/next/src/build/entries.ts | 8 +-- .../src/build/templates/edge-app-route.ts | 8 ++- .../next/src/build/templates/edge-ssr-app.ts | 8 +-- packages/next/src/build/templates/edge-ssr.ts | 11 ++- .../next-edge-app-route-loader/index.ts | 10 +-- .../loaders/next-edge-ssr-loader/index.ts | 12 ++-- .../server/web/edge-route-module-wrapper.ts | 11 +-- 11 files changed, 57 insertions(+), 109 deletions(-) diff --git a/crates/next-core/src/next_app/app_page_entry.rs b/crates/next-core/src/next_app/app_page_entry.rs index a95fd69c1820e3..5b6b47766ca422 100644 --- a/crates/next-core/src/next_app/app_page_entry.rs +++ b/crates/next-core/src/next_app/app_page_entry.rs @@ -145,17 +145,14 @@ async fn wrap_edge_page( ) -> Result>> { const INNER: &str = "INNER_PAGE_ENTRY"; - let next_config_val = &*next_config.await?; - let source = load_next_js_template( "edge-ssr-app.js", project_root.clone(), &[("VAR_USERLAND", INNER), ("VAR_PAGE", &page.to_string())], - &[ - // TODO do we really need to pass the entire next config here? - // This is bad for invalidation as any config change will invalidate this - ("nextConfig", &*serde_json::to_string(next_config_val)?), - ], + &[( + "cacheMaxMemorySize", + &*serde_json::to_string(&next_config.cache_max_memory_size().await?)?, + )], &[("incrementalCacheHandler", None)], ) .await?; diff --git a/crates/next-core/src/next_app/app_route_entry.rs b/crates/next-core/src/next_app/app_route_entry.rs index 7d8d3df74eaea7..783df543b46af3 100644 --- a/crates/next-core/src/next_app/app_route_entry.rs +++ b/crates/next-core/src/next_app/app_route_entry.rs @@ -136,13 +136,14 @@ async fn wrap_edge_route( ) -> Result>> { let inner = rcstr!("INNER_ROUTE_ENTRY"); - let next_config = &*next_config.await?; - let source = load_next_js_template( "edge-app-route.js", project_root.clone(), &[("VAR_USERLAND", &*inner), ("VAR_PAGE", &page.to_string())], - &[("nextConfig", &*serde_json::to_string(next_config)?)], + &[( + "cacheLifeProfiles", + &*serde_json::to_string(&next_config.cache_life().await?)?, + )], &[], ) .await?; diff --git a/crates/next-core/src/next_config.rs b/crates/next-core/src/next_config.rs index ef603e73460d3d..f5514c73d18483 100644 --- a/crates/next-core/src/next_config.rs +++ b/crates/next-core/src/next_config.rs @@ -81,10 +81,11 @@ pub struct NextConfig { config_file: Option, config_file_name: RcStr, + cache_life: Option, /// In-memory cache size in bytes. /// /// If `cache_max_memory_size: 0` disables in-memory caching. - cache_max_memory_size: Option, + cache_max_memory_size: Option, /// custom path to a cache handler to use cache_handler: Option, cache_handlers: Option>, @@ -835,7 +836,6 @@ pub struct ExperimentalConfig { adjust_font_fallbacks_with_size_adjust: Option, after: Option, app_document_preloading: Option, - cache_life: Option>, case_sensitive_routes: Option, cpus: Option, cra_compat: Option, @@ -908,63 +908,6 @@ pub struct ExperimentalConfig { devtool_segment_explorer: Option, } -#[derive( - Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue, -)] -#[serde(rename_all = "camelCase")] -pub struct CacheLifeProfile { - #[serde(skip_serializing_if = "Option::is_none")] - pub stale: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub revalidate: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub expire: Option, -} - -#[test] -fn test_cache_life_profiles() { - let json = serde_json::json!({ - "cacheLife": { - "frequent": { - "stale": 19, - "revalidate": 100, - }, - } - }); - - let config: ExperimentalConfig = serde_json::from_value(json).unwrap(); - let mut expected_cache_life = FxIndexMap::default(); - - expected_cache_life.insert( - "frequent".to_string(), - CacheLifeProfile { - stale: Some(19), - revalidate: Some(100), - expire: None, - }, - ); - - assert_eq!(config.cache_life, Some(expected_cache_life)); -} - -#[test] -fn test_cache_life_profiles_invalid() { - let json = serde_json::json!({ - "cacheLife": { - "invalid": { - "stale": "invalid_value", - }, - } - }); - - let result: Result = serde_json::from_value(json); - - assert!( - result.is_err(), - "Deserialization should fail due to invalid 'stale' value type" - ); -} - #[derive( Clone, Debug, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue, )] @@ -1312,6 +1255,16 @@ impl NextConfig { Vc::cell(self.base_path.clone()) } + #[turbo_tasks::function] + pub fn cache_life(&self) -> Vc { + Vc::cell(self.cache_life.clone()) + } + + #[turbo_tasks::function] + pub fn cache_max_memory_size(&self) -> Vc { + Vc::cell(self.cache_max_memory_size.clone()) + } + #[turbo_tasks::function] pub fn cache_handler(&self, project_path: FileSystemPath) -> Result> { if let Some(handler) = &self.cache_handler { diff --git a/crates/next-core/src/next_pages/page_entry.rs b/crates/next-core/src/next_pages/page_entry.rs index 2ef14953eb4f9a..8bba33ecca5a7d 100644 --- a/crates/next-core/src/next_pages/page_entry.rs +++ b/crates/next-core/src/next_pages/page_entry.rs @@ -223,8 +223,6 @@ async fn wrap_edge_page( const INNER_ERROR: &str = "INNER_ERROR"; const INNER_ERROR_500: &str = "INNER_500"; - let next_config_val = &*next_config.await?; - let source = load_next_js_template( "edge-ssr.js", project_root.clone(), @@ -236,9 +234,10 @@ async fn wrap_edge_page( ("VAR_MODULE_GLOBAL_ERROR", INNER_ERROR), ], &[ - // TODO do we really need to pass the entire next config here? - // This is bad for invalidation as any config change will invalidate this - ("nextConfig", &*serde_json::to_string(next_config_val)?), + ( + "cacheMaxMemorySize", + &*serde_json::to_string(&next_config.cache_max_memory_size().await?)?, + ), ( "pageRouteModuleOptions", &serde_json::to_string(&get_route_module_options(page.clone(), pathname.clone()))?, diff --git a/packages/next/src/build/entries.ts b/packages/next/src/build/entries.ts index d50740cb897900..e48f0573b41ebc 100644 --- a/packages/next/src/build/entries.ts +++ b/packages/next/src/build/entries.ts @@ -616,7 +616,9 @@ export function getEdgeServerEntry(opts: { absolutePagePath: opts.absolutePagePath, page: opts.page, appDirLoader: Buffer.from(opts.appDirLoader || '').toString('base64'), - nextConfig: Buffer.from(JSON.stringify(opts.config)).toString('base64'), + cacheLifeProfiles: Buffer.from( + JSON.stringify(opts.config.cacheLife) + ).toString('base64'), preferredRegion: opts.preferredRegion, middlewareConfig: Buffer.from( JSON.stringify(opts.middlewareConfig || {}) @@ -677,9 +679,7 @@ export function getEdgeServerEntry(opts: { dev: opts.isDev, isServerComponent: opts.isServerComponent, page: opts.page, - stringifiedConfig: Buffer.from(JSON.stringify(opts.config)).toString( - 'base64' - ), + cacheMaxMemorySize: JSON.stringify(opts.config.cacheMaxMemorySize), pagesType: opts.pagesType, appDirLoader: Buffer.from(opts.appDirLoader || '').toString('base64'), sriEnabled: !opts.isDev && !!opts.config.experimental.sri?.algorithm, diff --git a/packages/next/src/build/templates/edge-app-route.ts b/packages/next/src/build/templates/edge-app-route.ts index 3401fdf7348fea..58a6701a3809a3 100644 --- a/packages/next/src/build/templates/edge-app-route.ts +++ b/packages/next/src/build/templates/edge-app-route.ts @@ -7,8 +7,8 @@ import { EdgeRouteModuleWrapper } from '../../server/web/edge-route-module-wrapp import * as module from 'VAR_USERLAND' // injected by the loader afterwards. -declare const nextConfig: NextConfigComplete -// INJECT:nextConfig +declare const cacheLifeProfiles: NextConfigComplete['cacheLife'] +// INJECT:cacheLifeProfiles const maybeJSONParse = (str?: string) => (str ? JSON.parse(str) : undefined) @@ -28,4 +28,6 @@ if (rscManifest && rscServerManifest) { export const ComponentMod = module -export default EdgeRouteModuleWrapper.wrap(module.routeModule, { nextConfig }) +export default EdgeRouteModuleWrapper.wrap(module.routeModule, { + cacheLifeProfiles, +}) diff --git a/packages/next/src/build/templates/edge-ssr-app.ts b/packages/next/src/build/templates/edge-ssr-app.ts index 91de090ee745a0..ba39c22871d0f0 100644 --- a/packages/next/src/build/templates/edge-ssr-app.ts +++ b/packages/next/src/build/templates/edge-ssr-app.ts @@ -5,7 +5,6 @@ import { IncrementalCache } from '../../server/lib/incremental-cache' import * as pageMod from 'VAR_USERLAND' import type { RequestData } from '../../server/web/types' -import type { NextConfigComplete } from '../../server/config-shared' import { setReferenceManifestsSingleton } from '../../server/app-render/encryption-utils' import { createServerModuleMap } from '../../server/app-render/action-utils' import { initializeCacheHandlers } from '../../server/use-cache/handlers' @@ -27,12 +26,12 @@ import { checkIsOnDemandRevalidate } from '../../server/api-utils' import { CloseController } from '../../server/web/web-on-close' declare const incrementalCacheHandler: any -declare const nextConfig: NextConfigComplete +declare const cacheMaxMemorySize: number // OPTIONAL_IMPORT:incrementalCacheHandler -// INJECT:nextConfig +// INJECT:cacheMaxMemorySize // Initialize the cache handlers interface. -initializeCacheHandlers(nextConfig.cacheMaxMemorySize) +initializeCacheHandlers(cacheMaxMemorySize) const maybeJSONParse = (str?: string) => (str ? JSON.parse(str) : undefined) @@ -77,6 +76,7 @@ async function requestHandler( const { query, params, + nextConfig, buildId, buildManifest, prerenderManifest, diff --git a/packages/next/src/build/templates/edge-ssr.ts b/packages/next/src/build/templates/edge-ssr.ts index 5431190f5906a8..ad3fdd40c90027 100644 --- a/packages/next/src/build/templates/edge-ssr.ts +++ b/packages/next/src/build/templates/edge-ssr.ts @@ -19,7 +19,6 @@ import RouteModule, { import { WebNextRequest, WebNextResponse } from '../../server/base-http/web' import type { RequestData } from '../../server/web/types' -import type { NextConfigComplete } from '../../server/config-shared' import type { NextFetchEvent } from '../../server/web/spec-extension/fetch-event' import type RenderResult from '../../server/render-result' import type { RenderResultMetadata } from '../../server/render-result' @@ -28,20 +27,17 @@ import { BaseServerSpan } from '../../server/lib/trace/constants' import { HTML_CONTENT_TYPE_HEADER } from '../../lib/constants' // injected by the loader afterwards. -declare const nextConfig: NextConfigComplete +declare const cacheMaxMemorySize: number declare const pageRouteModuleOptions: any declare const errorRouteModuleOptions: any declare const user500RouteModuleOptions: any -// INJECT:nextConfig +// INJECT:cacheMaxMemorySize // INJECT:pageRouteModuleOptions // INJECT:errorRouteModuleOptions // INJECT:user500RouteModuleOptions // Initialize the cache handlers interface. -initializeCacheHandlers(nextConfig.cacheMaxMemorySize) - -// expose this for the route-module -;(globalThis as any).nextConfig = nextConfig +initializeCacheHandlers(cacheMaxMemorySize) const pageMod = { ...userlandPage, @@ -106,6 +102,7 @@ async function requestHandler( const { query, params, + nextConfig, buildId, isNextDataRequest, buildManifest, diff --git a/packages/next/src/build/webpack/loaders/next-edge-app-route-loader/index.ts b/packages/next/src/build/webpack/loaders/next-edge-app-route-loader/index.ts index aac70f5c8519b4..1a96064516fe37 100644 --- a/packages/next/src/build/webpack/loaders/next-edge-app-route-loader/index.ts +++ b/packages/next/src/build/webpack/loaders/next-edge-app-route-loader/index.ts @@ -11,7 +11,7 @@ export type EdgeAppRouteLoaderQuery = { page: string appDirLoader: string preferredRegion: string | string[] | undefined - nextConfig: string + cacheLifeProfiles: string middlewareConfig: string cacheHandlers: string } @@ -24,7 +24,7 @@ const EdgeAppRouteLoader: webpack.LoaderDefinitionFunction = absolute500Path, absoluteErrorPath, isServerComponent, - stringifiedConfig: stringifiedConfigBase64, + cacheMaxMemorySize: cacheMaxMemorySizeStringified, appDirLoader: appDirLoaderBase64, pagesType, cacheHandler, @@ -94,10 +94,6 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction = Buffer.from(middlewareConfigBase64, 'base64').toString() ) - const stringifiedConfig = Buffer.from( - stringifiedConfigBase64 || '', - 'base64' - ).toString() const appDirLoader = Buffer.from( appDirLoaderBase64 || '', 'base64' @@ -161,7 +157,7 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction = VAR_PAGE: page, }, { - nextConfig: stringifiedConfig, + cacheMaxMemorySize: cacheMaxMemorySizeStringified, }, { incrementalCacheHandler: cacheHandler ?? null, @@ -178,7 +174,7 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction = VAR_MODULE_GLOBAL_ERROR: errorPath, }, { - nextConfig: stringifiedConfig, + cacheMaxMemorySize: cacheMaxMemorySizeStringified, pageRouteModuleOptions: JSON.stringify(getRouteModuleOptions(page)), errorRouteModuleOptions: JSON.stringify( getRouteModuleOptions('/_error') diff --git a/packages/next/src/server/web/edge-route-module-wrapper.ts b/packages/next/src/server/web/edge-route-module-wrapper.ts index 5af22eebcfcfaf..70ee7cc8b6ef15 100644 --- a/packages/next/src/server/web/edge-route-module-wrapper.ts +++ b/packages/next/src/server/web/edge-route-module-wrapper.ts @@ -18,7 +18,7 @@ import { getEdgePreviewProps } from './get-edge-preview-props' import type { NextConfigComplete } from '../config-shared' export interface WrapOptions { - nextConfig: NextConfigComplete + cacheLifeProfiles: NextConfigComplete['cacheLife'] } /** @@ -37,7 +37,7 @@ export class EdgeRouteModuleWrapper { */ private constructor( private readonly routeModule: AppRouteRouteModule, - private readonly nextConfig: NextConfigComplete + private readonly cacheLifeProfiles: NextConfigComplete['cacheLife'] ) { // TODO: (wyattjoh) possibly allow the module to define it's own matcher this.matcher = new RouteMatcher(routeModule.definition) @@ -54,7 +54,10 @@ export class EdgeRouteModuleWrapper { */ public static wrap(routeModule: AppRouteRouteModule, options: WrapOptions) { // Create the module wrapper. - const wrapper = new EdgeRouteModuleWrapper(routeModule, options.nextConfig) + const wrapper = new EdgeRouteModuleWrapper( + routeModule, + options.cacheLifeProfiles + ) // Return the wrapping function. return (opts: AdapterOptions) => { @@ -111,7 +114,7 @@ export class EdgeRouteModuleWrapper { experimental: { authInterrupts: !!process.env.__NEXT_EXPERIMENTAL_AUTH_INTERRUPTS, }, - cacheLifeProfiles: this.nextConfig.cacheLife, + cacheLifeProfiles: this.cacheLifeProfiles, }, sharedContext: { buildId: '', // TODO: Populate this properly.