Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f2732e8
Use serialized next config for prod server
devjiwonchoi Sep 19, 2025
a3ae0bc
test: add test
devjiwonchoi Sep 19, 2025
14d14f1
match the manifest format
devjiwonchoi Sep 19, 2025
ee41b8c
update comment
devjiwonchoi Sep 19, 2025
5c3b362
errors.json
devjiwonchoi Sep 19, 2025
330b4f6
test: update to ensure it loads from serialized
devjiwonchoi Sep 19, 2025
1899b72
fall back to loading the config
devjiwonchoi Sep 19, 2025
3a40f56
test: update tests
devjiwonchoi Sep 19, 2025
5431ec6
test: update test/integration/config-experimental-warning/test/index.…
devjiwonchoi Sep 19, 2025
c6e5c03
test: fix lint
devjiwonchoi Sep 19, 2025
e632b94
test: fix lint
devjiwonchoi Sep 19, 2025
ac81ea2
test: i hate semi false
devjiwonchoi Sep 19, 2025
6a6ff0d
add serializeNextConfigForProduction flag
devjiwonchoi Sep 19, 2025
15b5f8e
test: disable serializeNextConfigForProduction for nx test
devjiwonchoi Sep 19, 2025
a782d26
update comment
devjiwonchoi Sep 19, 2025
9b98fb5
test: update comment
devjiwonchoi Sep 19, 2025
21d753b
config.experimental?.serializeNextConfigForProduction when write next…
devjiwonchoi Sep 19, 2025
b20019d
set serializeNextConfigForProduction to true on default config
devjiwonchoi Sep 19, 2025
7a63bfb
fix lint
devjiwonchoi Sep 19, 2025
bd9356a
add to schema
devjiwonchoi Sep 19, 2025
ca0b9bd
add __NEXT_EXPERIMENTAL_SERIALIZE_NEXT_CONFIG_FOR_PRODUCTION flag
devjiwonchoi Sep 20, 2025
a27a151
ci: enable __NEXT_EXPERIMENTAL_SERIALIZE_NEXT_CONFIG_FOR_PRODUCTION flag
devjiwonchoi Sep 20, 2025
0a25ac8
flatten logic
devjiwonchoi Sep 20, 2025
7a94269
test: enable serializeNextConfigForProduction
devjiwonchoi Sep 20, 2025
89edac0
use nullish coalescing
devjiwonchoi Sep 20, 2025
3830f49
update error json
devjiwonchoi Sep 20, 2025
d54197a
test: update tests
devjiwonchoi Sep 20, 2025
e999ffb
test: remove unnecessary addition
devjiwonchoi Sep 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -956,6 +956,7 @@ jobs:
afterBuild: |
export __NEXT_EXPERIMENTAL_PPR=true # for compatibility with the existing tests
export __NEXT_EXPERIMENTAL_CACHE_COMPONENTS=true
export __NEXT_EXPERIMENTAL_SERIALIZE_NEXT_CONFIG_FOR_PRODUCTION=true
export NEXT_EXTERNAL_TESTS_FILTERS="test/experimental-tests-manifest.json"

node run-tests.js \
Expand All @@ -978,6 +979,7 @@ jobs:
afterBuild: |
export __NEXT_EXPERIMENTAL_PPR=true # for compatibility with the existing tests
export __NEXT_EXPERIMENTAL_CACHE_COMPONENTS=true
export __NEXT_EXPERIMENTAL_SERIALIZE_NEXT_CONFIG_FOR_PRODUCTION=true
export NEXT_EXTERNAL_TESTS_FILTERS="test/experimental-tests-manifest.json"
export NEXT_TEST_MODE=dev

Expand All @@ -1002,6 +1004,7 @@ jobs:
afterBuild: |
export __NEXT_EXPERIMENTAL_PPR=true # for compatibility with the existing tests
export __NEXT_EXPERIMENTAL_CACHE_COMPONENTS=true
export __NEXT_EXPERIMENTAL_SERIALIZE_NEXT_CONFIG_FOR_PRODUCTION=true
export NEXT_EXTERNAL_TESTS_FILTERS="test/experimental-tests-manifest.json"
export NEXT_TEST_MODE=start

Expand Down
3 changes: 2 additions & 1 deletion packages/next/errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -808,5 +808,6 @@
"807": "Expected a %s response header.",
"808": "Invalid binary HMR message: insufficient data (expected %s bytes, got %s)",
"809": "Invalid binary HMR message of type %s",
"810": "React debug channel stream error"
"810": "React debug channel stream error",
"811": "Failed to load serialized config \"%s\"."
}
31 changes: 31 additions & 0 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import {
PRERENDER_MANIFEST,
REACT_LOADABLE_MANIFEST,
ROUTES_MANIFEST,
SERIALIZED_CONFIG_FILE,
SERVER_DIRECTORY,
SERVER_FILES_MANIFEST,
STATIC_STATUS_PAGES,
Expand Down Expand Up @@ -2723,6 +2724,36 @@ export default async function build(
requiredServerFilesManifest
)

// The required-server-files manifest contains serialized config, which can
// be loaded by the prod server. However, when a custom distDir is set,
// the prod server will not know the distDir until loading the config.
// Therefore we write the serialized config to the same directory as the
// original config file.
if (
config.output !== 'standalone' &&
config.distDir !== '.next' &&
// Use nullish coalescing (??) since we don't want to return when it's false.
(config.experimental?.serializeNextConfigForProduction ??
// This flag is used to be enabled on the tests.
process.env
.__NEXT_EXPERIMENTAL_SERIALIZE_NEXT_CONFIG_FOR_PRODUCTION ===
'true')
) {
const serializedConfigPath = path.join(
// Write to the same directory as the original config file.
config.configFile ? path.dirname(config.configFile) : dir,
SERIALIZED_CONFIG_FILE
)
await fs.writeFile(
serializedConfigPath,
JSON.stringify({
// To match the format of required server files manifest.
version: 1,
config: requiredServerFilesManifest.config,
})
)
}

// we don't need to inline for turbopack build as
// it will handle it's own caching separate of compile
if (isGenerateMode && !isTurbopack) {
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,7 @@ export const configSchema: zod.ZodType<NextConfig> = z.lazy(() =>
])
.optional(),
optimizeRouterScrolling: z.boolean().optional(),
serializeNextConfigForProduction: z.boolean().optional(),
})
.optional(),
exportPathMap: z
Expand Down
14 changes: 14 additions & 0 deletions packages/next/src/server/config-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,16 @@ export interface ExperimentalConfig {
* instead of `{distDir}`.
*/
isolatedDevBuild?: boolean

/**
* When enabled, the production server will use the serialized config file
* instead of the original config file. This can save the time of loading the
* config file, especially when you are using `next.config.ts`. When the `distDir`
* is set, the serialized config `next-config-serialized.json` will be written to
* the same directory as the original config file. This is because Next.js doesn't
* know the `distDir` until loading the config.
*/
serializeNextConfigForProduction?: boolean
Comment thread
devjiwonchoi marked this conversation as resolved.
}

export type ExportPathMap = {
Expand Down Expand Up @@ -1495,6 +1505,10 @@ export const defaultConfig = Object.freeze({
browserDebugInfoInTerminal: false,
optimizeRouterScrolling: false,
isolatedDevBuild: false,
serializeNextConfigForProduction:
// This flag is used to be enabled on the tests.
process.env.__NEXT_EXPERIMENTAL_SERIALIZE_NEXT_CONFIG_FOR_PRODUCTION ===
'true',
},
htmlLimitedBots: undefined,
bundlePagesRouterDependencies: false,
Expand Down
104 changes: 101 additions & 3 deletions packages/next/src/server/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { existsSync } from 'fs'
import { basename, extname, join, relative, isAbsolute, resolve } from 'path'
import { existsSync, readFileSync } from 'fs'
import {
basename,
extname,
join,
relative,
isAbsolute,
resolve,
dirname,
} from 'path'
import { pathToFileURL } from 'url'
import findUp from 'next/dist/compiled/find-up'
import * as Log from '../build/output/log'
Expand All @@ -9,6 +17,9 @@ import {
PHASE_DEVELOPMENT_SERVER,
PHASE_EXPORT,
PHASE_PRODUCTION_BUILD,
PHASE_PRODUCTION_SERVER,
SERVER_FILES_MANIFEST,
SERIALIZED_CONFIG_FILE,
type PHASE_TYPE,
} from '../shared/lib/constants'
import { defaultConfig, normalizeConfig } from './config-shared'
Expand Down Expand Up @@ -1344,6 +1355,93 @@ export default async function loadConfig(
return standaloneConfig
}

let path: string | undefined
// During prod server, we can load from the serialized config.
if (phase === PHASE_PRODUCTION_SERVER) {
// Try load from ".next" dir since we don't know the
// distDir until loading the config.
try {
const possiblyServerFilesManifestPath = join(
dir,
'.next',
SERVER_FILES_MANIFEST
)
if (existsSync(possiblyServerFilesManifestPath)) {
const parsed = JSON.parse(
readFileSync(possiblyServerFilesManifestPath, 'utf8')
)
const config: NextConfigComplete = parsed.config

if (
// Don't return here and will eventually fall back to loading the config.
// Use nullish coalescing (??) since we don't want to return when it's false.
config?.experimental?.serializeNextConfigForProduction ??
// This flag is used to be enabled on the tests.
process.env
.__NEXT_EXPERIMENTAL_SERIALIZE_NEXT_CONFIG_FOR_PRODUCTION === 'true'
) {
// Cache the config
configCache.set(cacheKey, {
config,
rawConfig: config,
configuredExperimentalFeatures: [],
})

return config
}
}
} catch {
// Continue to next option
}

// If the custom distDir is set, we write the serialized config
// to the same directory as the original config file.
path = await findUp(CONFIG_FILES, { cwd: dir })
const targetDir = path ? dirname(path) : dir

// Even though serializeNextConfigForProduction might be disabled, we still need to check
// the existSync because there's no way to know if serializeNextConfigForProduction
// is disabled until we load the config.
const serializedConfigPath = join(targetDir, SERIALIZED_CONFIG_FILE)
try {
if (existsSync(serializedConfigPath)) {
const parsed = JSON.parse(readFileSync(serializedConfigPath, 'utf8'))
const config: NextConfigComplete = parsed.config
Comment thread
devjiwonchoi marked this conversation as resolved.

if (
// Don't return here and will eventually fall back to loading the config.
// Use nullish coalescing (??) since we don't want to return when it's false.
config?.experimental?.serializeNextConfigForProduction ??
// This flag is used to be enabled on the tests.
process.env
.__NEXT_EXPERIMENTAL_SERIALIZE_NEXT_CONFIG_FOR_PRODUCTION === 'true'
) {
// Cache the config
configCache.set(cacheKey, {
config,
rawConfig: config,
configuredExperimentalFeatures: [],
})

return config
}
}
} catch (cause) {
if (
// This flag is used to be enabled on the tests.
process.env.__NEXT_EXPERIMENTAL_SERIALIZE_NEXT_CONFIG_FOR_PRODUCTION ===
'true'
) {
throw new Error(
`Failed to load serialized config "${serializedConfigPath}".`,
Comment thread
devjiwonchoi marked this conversation as resolved.
{ cause }
)
}
Comment thread
devjiwonchoi marked this conversation as resolved.
}

// Fall back to loading the config.
}
Comment thread
devjiwonchoi marked this conversation as resolved.

const curLog = silent
? {
warn: () => {},
Expand Down Expand Up @@ -1389,7 +1487,7 @@ export default async function loadConfig(
return config
}

const path = await findUp(CONFIG_FILES, { cwd: dir })
path ??= await findUp(CONFIG_FILES, { cwd: dir })

// If config file was found
if (path?.length) {
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/shared/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export const PRERENDER_MANIFEST = 'prerender-manifest.json'
export const ROUTES_MANIFEST = 'routes-manifest.json'
export const IMAGES_MANIFEST = 'images-manifest.json'
export const SERVER_FILES_MANIFEST = 'required-server-files.json'
export const SERIALIZED_CONFIG_FILE = 'next-config-serialized.json'
export const DEV_CLIENT_PAGES_MANIFEST = '_devPagesManifest.json'
export const MIDDLEWARE_MANIFEST = 'middleware-manifest.json'
export const TURBOPACK_CLIENT_MIDDLEWARE_MANIFEST =
Expand Down
7 changes: 7 additions & 0 deletions test/e2e/app-dir/nx-handling/apps/next-nx-test/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ const nextConfig = {
// Use this to set Nx-specific options
// See: https://nx.dev/recipes/next/next-config-setup
nx: {},
experimental: {
// Disable because nx tries to copy the config to the dist dir
// and expect to load the config inside the dist dir again.
// In this case, the `distDir` will be relative to the original config,
// not the config inside the dist dir, and cause distDir path mismatch.
serializeNextConfigForProduction: false,
},
}

const plugins = [
Expand Down
12 changes: 10 additions & 2 deletions test/e2e/config-schema-check/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,16 @@ describe('next.config.js schema validating - invalid config', () => {

expect(output).toContain('Invalid next.config.js options detected')
expect(output).toContain('badKey')
// for next start and next build we both display the warnings
expect(warningTimes).toBe(isNextStart ? 2 : 1)
if (
process.env.__NEXT_EXPERIMENTAL_SERIALIZE_NEXT_CONFIG_FOR_PRODUCTION ===
'true'
) {
// With serialized config, warnings only appear during build, not during start
expect(warningTimes).toBe(1)
} else {
// for next start and next build we both display the warnings
expect(warningTimes).toBe(isNextStart ? 2 : 1)
}

return 'success'
}, 'success')
Expand Down
12 changes: 10 additions & 2 deletions test/e2e/next-phase/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,16 @@ describe('next-phase', () => {
if (skipped) return

it('should render page with next phase correctly', async () => {
const isSerializedNextConfigForProduction =
process.env.__NEXT_EXPERIMENTAL_SERIALIZE_NEXT_CONFIG_FOR_PRODUCTION ===
'true'
const phases = {
dev: 'phase-development-server',
build: 'phase-production-build',
start: 'phase-production-server',
start: isSerializedNextConfigForProduction
? // Serialized next config will use the config from build.
'phase-production-build'
: 'phase-production-server',
}
const currentPhase = isNextDev ? phases.dev : phases.build
const nonExistedPhase = isNextDev ? phases.build : phases.dev
Expand All @@ -40,7 +46,9 @@ describe('next-phase', () => {
if (isNextDev) {
expect(next.cliOutput).not.toContain(phases.start)
} else {
expect(next.cliOutput).toContain(phases.start)
if (isSerializedNextConfigForProduction) {
expect(next.cliOutput).toContain(phases.start)
}
}
})
})
Loading
Loading