Skip to content

Commit 07d4e65

Browse files
committed
feat: add --experimental-cpu-prof flag with worker process profiling
- Add --experimental-cpu-prof to next dev, build, and start commands - Profiles are saved to .next/cpu-profiles/ on process exit - Support CPU profiling for worker processes: - webpack-build workers (server, client, edge-server) - turbopack-build worker - static worker - build-trace worker - Meaningful profile names for each component (e.g., build-main, build-webpack-server, dev-server) - Add integration and e2e tests verifying profile name patterns - Add CLI documentation
1 parent 207d6ce commit 07d4e65

File tree

21 files changed

+394
-16
lines changed

21 files changed

+394
-16
lines changed

docs/01-app/03-api-reference/06-cli/next.mdx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ The following commands are available:
5656
| `--experimental-https-cert <path>` | Path to a HTTPS certificate file. |
5757
| `--experimental-https-ca <path>` | Path to a HTTPS certificate authority file. |
5858
| `--experimental-upload-trace <traceUrl>` | Reports a subset of the debugging trace to a remote HTTP URL. |
59+
| `--experimental-cpu-prof` | Enables CPU profiling using V8's inspector. Profiles are saved to `.next/cpu-profiles/` on exit. |
5960

6061
### `next build` options
6162

@@ -87,6 +88,7 @@ The following options are available for the `next build` command:
8788
| `--experimental-build-mode [mode]` | Uses an experimental build mode. (choices: "compile", "generate", default: "default") |
8889
| `--debug-prerender` | Debug prerender errors in development. |
8990
| `--debug-build-paths=<patterns>` | Build only specific routes for debugging. |
91+
| `--experimental-cpu-prof` | Enables CPU profiling using V8's inspector. Profiles are saved to `.next/cpu-profiles/` on exit. |
9092

9193
### `next start` options
9294

@@ -101,6 +103,7 @@ The following options are available for the `next start` command:
101103
| `-p` or `--port <port>` | Specify a port number on which to start the application. (default: 3000, env: PORT) |
102104
| `-H` or `--hostname <hostname>` | Specify a hostname on which to start the application (default: 0.0.0.0). |
103105
| `--keepAliveTimeout <keepAliveTimeout>` | Specify the maximum amount of milliseconds to wait before closing the inactive connections. |
106+
| `--experimental-cpu-prof` | Enables CPU profiling using V8's inspector. Profiles are saved to `.next/cpu-profiles/` on exit. |
104107

105108
### `next info` options
106109

@@ -334,6 +337,25 @@ NODE_OPTIONS='-r esm' next
334337
NODE_OPTIONS='--inspect' next
335338
```
336339

340+
### CPU profiling
341+
342+
You can capture CPU profiles to analyze performance bottlenecks in your Next.js application. The `--experimental-cpu-prof` flag enables V8's built-in CPU profiler and saves profiles to `.next/cpu-profiles/` when the process exits:
343+
344+
```bash filename="Terminal"
345+
# Profile the build process
346+
next build --experimental-cpu-prof
347+
348+
# Profile the dev server (profile saved on Ctrl+C or SIGTERM)
349+
next dev --experimental-cpu-prof
350+
351+
# Profile the production server
352+
next start --experimental-cpu-prof
353+
```
354+
355+
The generated `.cpuprofile` files can be opened in Chrome DevTools (Performance tab → Load profile) or other V8-compatible profiling tools.
356+
357+
> **Good to know**: For `next dev`, both the parent process and the child server process are profiled separately, resulting in multiple profile files.
358+
337359
| Version | Changes |
338360
| --------- | ------------------------------------------------------------------------------- |
339361
| `v16.1.0` | Add the `next experimental-analyze` command |

packages/next/src/bin/next.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,15 +186,32 @@ program
186186
'--debug-build-paths <patterns>',
187187
'Comma-separated glob patterns or explicit paths for selective builds. Examples: "app/*", "app/page.tsx", "app/**/page.tsx"'
188188
)
189+
.option(
190+
'--experimental-cpu-prof',
191+
'Enable CPU profiling. Profile is saved to .next/cpu-profiles/ on completion.'
192+
)
189193
.action((directory: string, options: NextBuildOptions) => {
190194
if (options.experimentalNextConfigStripTypes) {
191195
process.env.__NEXT_NODE_NATIVE_TS_LOADER_ENABLED = 'true'
192196
}
197+
if (options.experimentalCpuProf) {
198+
process.env.NEXT_CPU_PROF = '1'
199+
process.env.__NEXT_PRIVATE_CPU_PROFILE = 'build-main'
200+
const { join } = require('path') as typeof import('path')
201+
const dir = directory || process.cwd()
202+
process.env.NEXT_CPU_PROF_DIR = join(dir, '.next', 'cpu-profiles')
203+
}
193204

194205
// ensure process exits after build completes so open handles/connections
195206
// don't cause process to hang
196207
return import('../cli/next-build.js').then((mod) =>
197-
mod.nextBuild(options, directory).then(() => process.exit(0))
208+
mod.nextBuild(options, directory).then(async () => {
209+
// Save CPU profile before exiting if enabled
210+
if (options.experimentalCpuProf) {
211+
await mod.saveCpuProfile()
212+
}
213+
process.exit(0)
214+
})
198215
)
199216
})
200217
.usage('[directory] [options]')
@@ -297,11 +314,22 @@ program
297314
'--experimental-next-config-strip-types',
298315
'Use Node.js native TypeScript resolution for next.config.(ts|mts)'
299316
)
317+
.option(
318+
'--experimental-cpu-prof',
319+
'Enable CPU profiling. Profiles are saved to .next/cpu-profiles/ on exit.'
320+
)
300321
.action(
301322
(directory: string, options: NextDevOptions, { _optionValueSources }) => {
302323
if (options.experimentalNextConfigStripTypes) {
303324
process.env.__NEXT_NODE_NATIVE_TS_LOADER_ENABLED = 'true'
304325
}
326+
if (options.experimentalCpuProf) {
327+
process.env.NEXT_CPU_PROF = '1'
328+
process.env.__NEXT_PRIVATE_CPU_PROFILE = 'dev-main'
329+
const { join } = require('path') as typeof import('path')
330+
const dir = directory || process.cwd()
331+
process.env.NEXT_CPU_PROF_DIR = join(dir, '.next', 'cpu-profiles')
332+
}
305333
const portSource = _optionValueSources.port
306334
import('../cli/next-dev.js').then((mod) =>
307335
mod.nextDev(options, portSource, directory)
@@ -363,10 +391,21 @@ program
363391
'--experimental-next-config-strip-types',
364392
'Use Node.js native TypeScript resolution for next.config.(ts|mts)'
365393
)
394+
.option(
395+
'--experimental-cpu-prof',
396+
'Enable CPU profiling. Profiles are saved to .next/cpu-profiles/ on exit.'
397+
)
366398
.action((directory: string, options: NextStartOptions) => {
367399
if (options.experimentalNextConfigStripTypes) {
368400
process.env.__NEXT_NODE_NATIVE_TS_LOADER_ENABLED = 'true'
369401
}
402+
if (options.experimentalCpuProf) {
403+
process.env.NEXT_CPU_PROF = '1'
404+
process.env.__NEXT_PRIVATE_CPU_PROFILE = 'start-main'
405+
const { join } = require('path') as typeof import('path')
406+
const dir = directory || process.cwd()
407+
process.env.NEXT_CPU_PROF_DIR = join(dir, '.next', 'cpu-profiles')
408+
}
370409
return import('../cli/next-start.js').then((mod) =>
371410
mod.nextStart(options, directory)
372411
)

packages/next/src/build/collect-build-traces.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// Import cpu-profile to start profiling early if enabled
2+
import '../server/lib/cpu-profile'
13
import { Span } from '../trace'
24
import type { NextConfigComplete } from '../server/config-shared'
35

packages/next/src/build/index.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,15 @@ export function createStaticWorker(
831831
isolatedMemory: true,
832832
enableWorkerThreads: config.experimental.workerThreads,
833833
exposedMethods: staticWorkerExposedMethods,
834+
forkOptions: process.env.NEXT_CPU_PROF
835+
? {
836+
env: {
837+
NEXT_CPU_PROF: '1',
838+
NEXT_CPU_PROF_DIR: process.env.NEXT_CPU_PROF_DIR,
839+
__NEXT_PRIVATE_CPU_PROFILE: 'build-static-worker',
840+
},
841+
}
842+
: undefined,
834843
}) as StaticWorker
835844
}
836845

@@ -1735,6 +1744,15 @@ export default async function build(
17351744
isolatedMemory: false,
17361745
numWorkers: 1,
17371746
exposedMethods: ['collectBuildTraces'],
1747+
forkOptions: process.env.NEXT_CPU_PROF
1748+
? {
1749+
env: {
1750+
NEXT_CPU_PROF: '1',
1751+
NEXT_CPU_PROF_DIR: process.env.NEXT_CPU_PROF_DIR,
1752+
__NEXT_PRIVATE_CPU_PROFILE: 'build-trace-worker',
1753+
},
1754+
}
1755+
: undefined,
17381756
}
17391757
) as Worker & typeof import('./collect-build-traces')
17401758

packages/next/src/build/turbopack-build/impl.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// Import cpu-profile first to start profiling early if enabled
2+
import { saveCpuProfile } from '../../server/lib/cpu-profile'
13
import path from 'path'
24
import { validateTurboNextConfig } from '../../lib/turbopack-warning'
35
import { isFileSystemCacheEnabledForBuild } from '../../shared/lib/turbopack/utils'
@@ -261,6 +263,8 @@ export async function workerMain(workerData: {
261263
} finally {
262264
// Always flush telemetry before worker exits (waits for async operations like setTimeout in debug mode)
263265
await telemetry.flush()
266+
// Save CPU profile before worker exits
267+
await saveCpuProfile()
264268
}
265269
}
266270

packages/next/src/build/turbopack-build/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ async function turbopackBuildWithWorker(): ReturnType<
1717
forkOptions: {
1818
env: {
1919
NEXT_PRIVATE_BUILD_WORKER: '1',
20+
...(process.env.NEXT_CPU_PROF
21+
? {
22+
NEXT_CPU_PROF: '1',
23+
NEXT_CPU_PROF_DIR: process.env.NEXT_CPU_PROF_DIR,
24+
__NEXT_PRIVATE_CPU_PROFILE: 'build-turbopack',
25+
}
26+
: undefined),
2027
},
2128
},
2229
}) as Worker & typeof import('./impl')

packages/next/src/build/webpack-build/impl.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// Import cpu-profile first to start profiling early if enabled
2+
import { saveCpuProfile } from '../../server/lib/cpu-profile'
13
import type { webpack } from 'next/dist/compiled/webpack/webpack'
24
import { stringBufferUtils } from 'next/dist/compiled/webpack-sources3'
35
import { red } from '../../lib/picocolors'
@@ -416,5 +418,8 @@ export async function workerMain(workerData: {
416418
NextBuildContext.nextBuildSpan.stop()
417419
await telemetry.flush()
418420

421+
// Save CPU profile before worker exits
422+
await saveCpuProfile()
423+
419424
return { ...result, debugTraceEvents: getTraceEvents() }
420425
}

packages/next/src/build/webpack-build/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ async function webpackBuildWithWorker(
5454
forkOptions: {
5555
env: {
5656
NEXT_PRIVATE_BUILD_WORKER: '1',
57+
...(process.env.NEXT_CPU_PROF
58+
? {
59+
NEXT_CPU_PROF: '1',
60+
NEXT_CPU_PROF_DIR: process.env.NEXT_CPU_PROF_DIR,
61+
__NEXT_PRIVATE_CPU_PROFILE: `build-webpack-${compilerName}`,
62+
}
63+
: undefined),
5764
},
5865
},
5966
}) as Worker & typeof import('./impl')

packages/next/src/build/worker.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import '../server/require-hook'
2+
// Import cpu-profile to start profiling early if enabled
3+
import '../server/lib/cpu-profile'
24

35
export {
46
getDefinedNamedExports,

packages/next/src/cli/next-build.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env node
22

3-
import '../server/lib/cpu-profile'
3+
import { saveCpuProfile } from '../server/lib/cpu-profile'
44
import { existsSync } from 'fs'
55
import { italic } from '../lib/picocolors'
66
import build from '../build'
@@ -32,6 +32,7 @@ export type NextBuildOptions = {
3232
experimentalUploadTrace?: string
3333
experimentalNextConfigStripTypes?: boolean
3434
debugBuildPaths?: string
35+
experimentalCpuProf?: boolean
3536
}
3637

3738
const nextBuild = async (options: NextBuildOptions, directory?: string) => {
@@ -157,4 +158,4 @@ const nextBuild = async (options: NextBuildOptions, directory?: string) => {
157158
})
158159
}
159160

160-
export { nextBuild }
161+
export { nextBuild, saveCpuProfile }

0 commit comments

Comments
 (0)