From 914cac735fe946e6182cb505eaae7eeff172107c Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 9 Mar 2026 14:34:03 +0000 Subject: [PATCH 01/13] feat(build,dev): add `--profile` support --- packages/nuxi/src/commands/_shared.ts | 9 +++++++++ packages/nuxi/src/commands/build.ts | 9 ++++++++- packages/nuxi/src/commands/dev.ts | 3 ++- packages/nuxi/src/commands/generate.ts | 3 ++- packages/nuxi/src/dev/index.ts | 8 ++++++++ packages/nuxi/src/dev/utils.ts | 1 + 6 files changed, 30 insertions(+), 3 deletions(-) diff --git a/packages/nuxi/src/commands/_shared.ts b/packages/nuxi/src/commands/_shared.ts index 7d10a1c0d..28c2c359b 100644 --- a/packages/nuxi/src/commands/_shared.ts +++ b/packages/nuxi/src/commands/_shared.ts @@ -40,6 +40,15 @@ export const extendsArgs = { }, } as const satisfies Record +export const profileArgs = { + profile: { + type: 'string', + description: 'Profile performance. Use --profile for CPU only, --profile=verbose for full report.', + default: false as unknown as string, + valueHint: 'verbose', + }, +} as const satisfies Record + export const legacyRootDirArgs = { // cwd falls back to rootDir's default (indirect default) cwd: { diff --git a/packages/nuxi/src/commands/build.ts b/packages/nuxi/src/commands/build.ts index fb9b6ec61..d9f9bc6d1 100644 --- a/packages/nuxi/src/commands/build.ts +++ b/packages/nuxi/src/commands/build.ts @@ -10,7 +10,7 @@ import { overrideEnv } from '../utils/env' import { clearBuildDir } from '../utils/fs' import { loadKit } from '../utils/kit' import { logger } from '../utils/logger' -import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared' +import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs, profileArgs } from './_shared' export default defineCommand({ meta: { @@ -31,6 +31,7 @@ export default defineCommand({ ...dotEnvArgs, ...envNameArgs, ...extendsArgs, + ...profileArgs, ...legacyRootDirArgs, }, async run(ctx) { @@ -43,6 +44,11 @@ export default defineCommand({ const kit = await loadKit(cwd) await showVersions(cwd, kit, ctx.args.dotenv) + // --profile → CPU profile only (quiet), --profile=verbose → full report + const buildStart = Date.now() + const profileArg = ctx.args.profile + const perfValue = profileArg === 'verbose' ? true : profileArg ? 'quiet' : undefined + const nuxt = await kit.loadNuxt({ cwd, dotenv: { @@ -59,6 +65,7 @@ export default defineCommand({ preset: ctx.args.preset || process.env.NITRO_PRESET || process.env.SERVER_PRESET, }, ...(ctx.args.extends && { extends: ctx.args.extends }), + ...(perfValue && { debug: { perf: perfValue }, _startTime: buildStart }), ...ctx.data?.overrides, }, }) diff --git a/packages/nuxi/src/commands/dev.ts b/packages/nuxi/src/commands/dev.ts index 9101f7da9..c8fe5db54 100644 --- a/packages/nuxi/src/commands/dev.ts +++ b/packages/nuxi/src/commands/dev.ts @@ -13,7 +13,7 @@ import { isBun, isTest } from 'std-env' import { initialize } from '../dev' import { ForkPool } from '../dev/pool' import { debug, logger } from '../utils/logger' -import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared' +import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs, profileArgs } from './_shared' const startTime: number | undefined = Date.now() const forkSupported = !isTest && (!isBun || isBunForkSupported()) @@ -62,6 +62,7 @@ const command = defineCommand({ }, clipboard: { ...listhenArgs.clipboard, default: false }, }, + ...profileArgs, sslCert: { type: 'string', description: '(DEPRECATED) Use `--https.cert` instead.', diff --git a/packages/nuxi/src/commands/generate.ts b/packages/nuxi/src/commands/generate.ts index 6e2b9015f..2ee64d2c4 100644 --- a/packages/nuxi/src/commands/generate.ts +++ b/packages/nuxi/src/commands/generate.ts @@ -1,6 +1,6 @@ import { defineCommand } from 'citty' -import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared' +import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs, profileArgs } from './_shared' import buildCommand from './build' export default defineCommand({ @@ -18,6 +18,7 @@ export default defineCommand({ ...dotEnvArgs, ...envNameArgs, ...extendsArgs, + ...profileArgs, ...legacyRootDirArgs, }, async run(ctx) { diff --git a/packages/nuxi/src/dev/index.ts b/packages/nuxi/src/dev/index.ts index 8a8f2f748..5dacf70e6 100644 --- a/packages/nuxi/src/dev/index.ts +++ b/packages/nuxi/src/dev/index.ts @@ -55,11 +55,19 @@ interface InitializeReturn { export async function initialize(devContext: NuxtDevContext, ctx: InitializeOptions = {}): Promise { overrideEnv('development') + // --profile → CPU profile only (quiet), --profile=verbose → full report + const profileArg = devContext.args.profile + const perfValue = profileArg === 'verbose' ? true : profileArg ? 'quiet' : undefined + const perfOverrides = perfValue + ? { debug: { perf: perfValue }, _startTime: start } as NuxtConfig + : {} + const devServer = new NuxtDevServer({ cwd: devContext.cwd, overrides: defu( ctx.data?.overrides, ({ extends: devContext.args.extends } satisfies NuxtConfig) as NuxtConfig, + perfOverrides, ), logLevel: devContext.args.logLevel as 'silent' | 'info' | 'verbose', clear: devContext.args.clear, diff --git a/packages/nuxi/src/dev/utils.ts b/packages/nuxi/src/dev/utils.ts index 6c262eff9..9ad83e93f 100644 --- a/packages/nuxi/src/dev/utils.ts +++ b/packages/nuxi/src/dev/utils.ts @@ -48,6 +48,7 @@ export interface NuxtDevContext { dotenv?: string envName?: string extends?: string + profile?: string | boolean } } From 2680bb6e9453da326a61239c51df41a990e7990d Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 9 Mar 2026 17:25:40 +0000 Subject: [PATCH 02/13] fix: don't set _startTime --- packages/nuxi/src/commands/build.ts | 2 +- packages/nuxi/src/dev/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nuxi/src/commands/build.ts b/packages/nuxi/src/commands/build.ts index d9f9bc6d1..d6d69cec6 100644 --- a/packages/nuxi/src/commands/build.ts +++ b/packages/nuxi/src/commands/build.ts @@ -65,7 +65,7 @@ export default defineCommand({ preset: ctx.args.preset || process.env.NITRO_PRESET || process.env.SERVER_PRESET, }, ...(ctx.args.extends && { extends: ctx.args.extends }), - ...(perfValue && { debug: { perf: perfValue }, _startTime: buildStart }), + ...(perfValue && { debug: { perf: perfValue } }), ...ctx.data?.overrides, }, }) diff --git a/packages/nuxi/src/dev/index.ts b/packages/nuxi/src/dev/index.ts index 5dacf70e6..7f197af9b 100644 --- a/packages/nuxi/src/dev/index.ts +++ b/packages/nuxi/src/dev/index.ts @@ -59,7 +59,7 @@ export async function initialize(devContext: NuxtDevContext, ctx: InitializeOpti const profileArg = devContext.args.profile const perfValue = profileArg === 'verbose' ? true : profileArg ? 'quiet' : undefined const perfOverrides = perfValue - ? { debug: { perf: perfValue }, _startTime: start } as NuxtConfig + ? { debug: { perf: perfValue } } as NuxtConfig : {} const devServer = new NuxtDevServer({ From 338aebf20b048f379c8432c33d65dcfd188d2deb Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 9 Mar 2026 17:33:51 +0000 Subject: [PATCH 03/13] fix: use `undefined` as default `profile` value --- packages/nuxi/src/commands/_shared.ts | 2 +- packages/nuxi/src/commands/build.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/nuxi/src/commands/_shared.ts b/packages/nuxi/src/commands/_shared.ts index 28c2c359b..8606274aa 100644 --- a/packages/nuxi/src/commands/_shared.ts +++ b/packages/nuxi/src/commands/_shared.ts @@ -44,7 +44,7 @@ export const profileArgs = { profile: { type: 'string', description: 'Profile performance. Use --profile for CPU only, --profile=verbose for full report.', - default: false as unknown as string, + default: undefined as string | undefined, valueHint: 'verbose', }, } as const satisfies Record diff --git a/packages/nuxi/src/commands/build.ts b/packages/nuxi/src/commands/build.ts index d6d69cec6..b1bae920c 100644 --- a/packages/nuxi/src/commands/build.ts +++ b/packages/nuxi/src/commands/build.ts @@ -45,7 +45,6 @@ export default defineCommand({ await showVersions(cwd, kit, ctx.args.dotenv) // --profile → CPU profile only (quiet), --profile=verbose → full report - const buildStart = Date.now() const profileArg = ctx.args.profile const perfValue = profileArg === 'verbose' ? true : profileArg ? 'quiet' : undefined From 39aad905d2a2ade93672d43eafa21fd0000fd78f Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 9 Mar 2026 20:54:10 +0000 Subject: [PATCH 04/13] feat: move profiler entirely to cli --- packages/nuxi/bin/nuxi.mjs | 16 +++++ packages/nuxi/src/commands/build.ts | 15 ++++- packages/nuxi/src/dev/index.ts | 10 ++- packages/nuxi/src/utils/profile.ts | 99 +++++++++++++++++++++++++++++ packages/nuxt-cli/bin/nuxi.mjs | 16 +++++ types.d.ts | 1 + 6 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 packages/nuxi/src/utils/profile.ts diff --git a/packages/nuxi/bin/nuxi.mjs b/packages/nuxi/bin/nuxi.mjs index 63504b583..259422031 100755 --- a/packages/nuxi/bin/nuxi.mjs +++ b/packages/nuxi/bin/nuxi.mjs @@ -1,5 +1,6 @@ #!/usr/bin/env node +import inspector from 'node:inspector' import nodeModule from 'node:module' import process from 'node:process' import { fileURLToPath } from 'node:url' @@ -25,6 +26,21 @@ globalThis.__nuxt_cli__ = { devEntry: fileURLToPath(new URL('../dist/dev/index.mjs', import.meta.url)), } +if ( + process.argv.includes('--profile') + || process.argv.some(a => a.startsWith('--profile=')) +) { + const session = new inspector.Session() + session.connect() + // eslint-disable-next-line antfu/no-top-level-await + await new Promise((resolve) => { + session.post('Profiler.enable', () => { + session.post('Profiler.start', resolve) + }) + }) + globalThis.__nuxt_cli__.cpuProfileSession = session +} + // eslint-disable-next-line antfu/no-top-level-await const { runMain } = await import('../dist/index.mjs') diff --git a/packages/nuxi/src/commands/build.ts b/packages/nuxi/src/commands/build.ts index b1bae920c..2990f7bbf 100644 --- a/packages/nuxi/src/commands/build.ts +++ b/packages/nuxi/src/commands/build.ts @@ -10,6 +10,7 @@ import { overrideEnv } from '../utils/env' import { clearBuildDir } from '../utils/fs' import { loadKit } from '../utils/kit' import { logger } from '../utils/logger' +import { installSignalHandlers, startCpuProfile, stopCpuProfile } from '../utils/profile' import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs, profileArgs } from './_shared' export default defineCommand({ @@ -39,14 +40,18 @@ export default defineCommand({ const cwd = resolve(ctx.args.cwd || ctx.args.rootDir) + const profileArg = ctx.args.profile + const perfValue = profileArg === 'verbose' ? true : profileArg ? 'quiet' : undefined + if (profileArg) { + await startCpuProfile() + installSignalHandlers(cwd) + } + intro(colors.cyan('Building Nuxt for production...')) const kit = await loadKit(cwd) await showVersions(cwd, kit, ctx.args.dotenv) - // --profile → CPU profile only (quiet), --profile=verbose → full report - const profileArg = ctx.args.profile - const perfValue = profileArg === 'verbose' ? true : profileArg ? 'quiet' : undefined const nuxt = await kit.loadNuxt({ cwd, @@ -93,6 +98,10 @@ export default defineCommand({ await kit.buildNuxt(nuxt) + if (profileArg) { + await stopCpuProfile(cwd).catch(() => {}) + } + if (ctx.args.prerender) { if (!nuxt.options.ssr) { logger.warn(`HTML content not prerendered because ${colors.cyan('ssr: false')} was set.`) diff --git a/packages/nuxi/src/dev/index.ts b/packages/nuxi/src/dev/index.ts index 7f197af9b..af3ecce94 100644 --- a/packages/nuxi/src/dev/index.ts +++ b/packages/nuxi/src/dev/index.ts @@ -5,6 +5,7 @@ import type { NuxtDevContext, NuxtDevIPCMessage, NuxtParentIPCMessage } from './ import process from 'node:process' import defu from 'defu' import { overrideEnv } from '../utils/env.ts' +import { installSignalHandlers, startCpuProfile, stopCpuProfile } from '../utils/profile.ts' import { NuxtDevServer } from './utils' const start = Date.now() @@ -55,13 +56,17 @@ interface InitializeReturn { export async function initialize(devContext: NuxtDevContext, ctx: InitializeOptions = {}): Promise { overrideEnv('development') - // --profile → CPU profile only (quiet), --profile=verbose → full report const profileArg = devContext.args.profile const perfValue = profileArg === 'verbose' ? true : profileArg ? 'quiet' : undefined const perfOverrides = perfValue ? { debug: { perf: perfValue } } as NuxtConfig : {} + if (profileArg) { + await startCpuProfile() + installSignalHandlers(devContext.cwd) + } + const devServer = new NuxtDevServer({ cwd: devContext.cwd, overrides: defu( @@ -123,6 +128,9 @@ export async function initialize(devContext: NuxtDevContext, ctx: InitializeOpti devServer.listener.close(), devServer.close(), ]) + if (profileArg) { + await stopCpuProfile(devContext.cwd).catch(() => {}) + } }, onReady: (callback: (address: string) => void) => { if (address) { diff --git a/packages/nuxi/src/utils/profile.ts b/packages/nuxi/src/utils/profile.ts new file mode 100644 index 000000000..2d8ca95d0 --- /dev/null +++ b/packages/nuxi/src/utils/profile.ts @@ -0,0 +1,99 @@ +import type { Session } from 'node:inspector' +import { mkdirSync, writeFileSync } from 'node:fs' +import { mkdir, writeFile } from 'node:fs/promises' +import process from 'node:process' +import { colors } from 'consola/utils' +import { join } from 'pathe' +import { logger } from './logger' + +let session: Session | undefined +let profileCount = 0 + +export async function startCpuProfile(): Promise { + // Adopt session started in bin/nuxi.mjs + const cli = globalThis.__nuxt_cli__ as typeof globalThis.__nuxt_cli__ & { cpuProfileSession?: import('node:inspector').Session } + if (cli?.cpuProfileSession) { + session = cli.cpuProfileSession + delete cli.cpuProfileSession + return + } + const inspector = await import('node:inspector') + session = new inspector.Session() + session.connect() + await new Promise((res, rej) => { + session!.post('Profiler.enable', () => { + session!.post('Profiler.start', (err) => { + if (err) { + rej(err) + } + else { res() } + }) + }) + }) +} + +export function stopCpuProfile(outDir: string): Promise { + if (!session) { + return Promise.resolve(undefined) + } + const s = session + session = undefined + return new Promise((res, rej) => { + s.post('Profiler.stop', (err, { profile }) => { + if (err) { + return rej(err) + } + const outPath = join(outDir, `profile-${profileCount++}.cpuprofile`) + mkdir(outDir, { recursive: true }) + .then(() => writeFile(outPath, JSON.stringify(profile))) + .then(() => { + logger.info(`CPU profile written to ${colors.cyan(outPath)}`) + logger.info(`Open it in ${colors.cyan('https://www.speedscope.app')} or Chrome DevTools`) + s.disconnect() + res(outPath) + }) + .catch(rej) + }) + }) +} + +export function stopCpuProfileSync(outDir: string): string | undefined { + if (!session) { + return + } + const s = session + session = undefined + let outPath: string | undefined + s.post('Profiler.stop', (_err, params) => { + if (_err || !params?.profile) { + return + } + outPath = join(outDir, `profile-${profileCount++}.cpuprofile`) + try { + mkdirSync(outDir, { recursive: true }) + writeFileSync(outPath, JSON.stringify(params.profile)) + logger.info(`CPU profile written to ${colors.cyan(outPath)}`) + logger.info(`Open it in ${colors.cyan('https://www.speedscope.app')} or Chrome DevTools`) + } + catch {} + s.disconnect() + }) + return outPath +} + +/** + * Install signal handlers that flush the CPU profile before exit. + * Returns a cleanup function to remove the handlers. + */ +export function installSignalHandlers(outDir: string): () => void { + const onSignal = (signal: NodeJS.Signals) => { + stopCpuProfileSync(outDir) + process.kill(process.pid, signal) + } + process.once('SIGINT', onSignal) + process.once('SIGTERM', onSignal) + return () => { + process.off('SIGINT', onSignal) + process.off('SIGTERM', onSignal) + } +} diff --git a/packages/nuxt-cli/bin/nuxi.mjs b/packages/nuxt-cli/bin/nuxi.mjs index 63504b583..259422031 100755 --- a/packages/nuxt-cli/bin/nuxi.mjs +++ b/packages/nuxt-cli/bin/nuxi.mjs @@ -1,5 +1,6 @@ #!/usr/bin/env node +import inspector from 'node:inspector' import nodeModule from 'node:module' import process from 'node:process' import { fileURLToPath } from 'node:url' @@ -25,6 +26,21 @@ globalThis.__nuxt_cli__ = { devEntry: fileURLToPath(new URL('../dist/dev/index.mjs', import.meta.url)), } +if ( + process.argv.includes('--profile') + || process.argv.some(a => a.startsWith('--profile=')) +) { + const session = new inspector.Session() + session.connect() + // eslint-disable-next-line antfu/no-top-level-await + await new Promise((resolve) => { + session.post('Profiler.enable', () => { + session.post('Profiler.start', resolve) + }) + }) + globalThis.__nuxt_cli__.cpuProfileSession = session +} + // eslint-disable-next-line antfu/no-top-level-await const { runMain } = await import('../dist/index.mjs') diff --git a/types.d.ts b/types.d.ts index 2893c86a3..6ac85db41 100644 --- a/types.d.ts +++ b/types.d.ts @@ -6,6 +6,7 @@ declare global { entry: string devEntry?: string startTime: number + cpuProfileSession?: import('node:inspector').Session } } From 72559f9486f28d5c4f25c8d97eeb13a6a4fb135c Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 9 Mar 2026 20:55:11 +0000 Subject: [PATCH 05/13] fix: merge debug overrides --- packages/nuxi/src/commands/build.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/nuxi/src/commands/build.ts b/packages/nuxi/src/commands/build.ts index 2990f7bbf..7a18b3ba7 100644 --- a/packages/nuxi/src/commands/build.ts +++ b/packages/nuxi/src/commands/build.ts @@ -69,7 +69,12 @@ export default defineCommand({ preset: ctx.args.preset || process.env.NITRO_PRESET || process.env.SERVER_PRESET, }, ...(ctx.args.extends && { extends: ctx.args.extends }), - ...(perfValue && { debug: { perf: perfValue } }), + ...((perfValue || ctx.data?.overrides?.debug) && { + debug: { + ...ctx.data?.overrides?.debug, + ...(perfValue && { perf: perfValue }), + }, + }), ...ctx.data?.overrides, }, }) From cd04391c3a1e6764395f29a2e05a9b817c89cfa4 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 9 Mar 2026 20:57:18 +0000 Subject: [PATCH 06/13] chore: lint --- packages/nuxi/src/utils/profile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxi/src/utils/profile.ts b/packages/nuxi/src/utils/profile.ts index 2d8ca95d0..c284c431e 100644 --- a/packages/nuxi/src/utils/profile.ts +++ b/packages/nuxi/src/utils/profile.ts @@ -57,7 +57,7 @@ export function stopCpuProfile(outDir: string): Promise { }) } -export function stopCpuProfileSync(outDir: string): string | undefined { +function stopCpuProfileSync(outDir: string): string | undefined { if (!session) { return } From 74916ddb032d60cbc3208f4eb204140f9ac971f8 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 10 Mar 2026 05:34:54 +0000 Subject: [PATCH 07/13] fix: stop profiler synchronously --- packages/nuxi/src/commands/build.ts | 7 ++-- packages/nuxi/src/dev/index.ts | 10 +++--- packages/nuxi/src/utils/profile.ts | 53 +++-------------------------- 3 files changed, 12 insertions(+), 58 deletions(-) diff --git a/packages/nuxi/src/commands/build.ts b/packages/nuxi/src/commands/build.ts index 7a18b3ba7..bdf92dbaa 100644 --- a/packages/nuxi/src/commands/build.ts +++ b/packages/nuxi/src/commands/build.ts @@ -10,7 +10,7 @@ import { overrideEnv } from '../utils/env' import { clearBuildDir } from '../utils/fs' import { loadKit } from '../utils/kit' import { logger } from '../utils/logger' -import { installSignalHandlers, startCpuProfile, stopCpuProfile } from '../utils/profile' +import { startCpuProfile, stopCpuProfile } from '../utils/profile' import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs, profileArgs } from './_shared' export default defineCommand({ @@ -44,7 +44,6 @@ export default defineCommand({ const perfValue = profileArg === 'verbose' ? true : profileArg ? 'quiet' : undefined if (profileArg) { await startCpuProfile() - installSignalHandlers(cwd) } intro(colors.cyan('Building Nuxt for production...')) @@ -103,9 +102,7 @@ export default defineCommand({ await kit.buildNuxt(nuxt) - if (profileArg) { - await stopCpuProfile(cwd).catch(() => {}) - } + stopCpuProfile(cwd) if (ctx.args.prerender) { if (!nuxt.options.ssr) { diff --git a/packages/nuxi/src/dev/index.ts b/packages/nuxi/src/dev/index.ts index af3ecce94..dbeb02279 100644 --- a/packages/nuxi/src/dev/index.ts +++ b/packages/nuxi/src/dev/index.ts @@ -5,7 +5,7 @@ import type { NuxtDevContext, NuxtDevIPCMessage, NuxtParentIPCMessage } from './ import process from 'node:process' import defu from 'defu' import { overrideEnv } from '../utils/env.ts' -import { installSignalHandlers, startCpuProfile, stopCpuProfile } from '../utils/profile.ts' +import { startCpuProfile, stopCpuProfile } from '../utils/profile.ts' import { NuxtDevServer } from './utils' const start = Date.now() @@ -64,7 +64,6 @@ export async function initialize(devContext: NuxtDevContext, ctx: InitializeOpti if (profileArg) { await startCpuProfile() - installSignalHandlers(devContext.cwd) } const devServer = new NuxtDevServer({ @@ -120,6 +119,10 @@ export async function initialize(devContext: NuxtDevContext, ctx: InitializeOpti console.debug(`Dev server (internal) initialized in ${Date.now() - start}ms`) } + if (profileArg) { + process.once('exit', () => stopCpuProfile(devContext.cwd)) + } + return { listener: devServer.listener, close: async () => { @@ -128,9 +131,6 @@ export async function initialize(devContext: NuxtDevContext, ctx: InitializeOpti devServer.listener.close(), devServer.close(), ]) - if (profileArg) { - await stopCpuProfile(devContext.cwd).catch(() => {}) - } }, onReady: (callback: (address: string) => void) => { if (address) { diff --git a/packages/nuxi/src/utils/profile.ts b/packages/nuxi/src/utils/profile.ts index c284c431e..b9fda11e0 100644 --- a/packages/nuxi/src/utils/profile.ts +++ b/packages/nuxi/src/utils/profile.ts @@ -1,7 +1,5 @@ import type { Session } from 'node:inspector' import { mkdirSync, writeFileSync } from 'node:fs' -import { mkdir, writeFile } from 'node:fs/promises' -import process from 'node:process' import { colors } from 'consola/utils' import { join } from 'pathe' import { logger } from './logger' @@ -10,8 +8,7 @@ let session: Session | undefined let profileCount = 0 export async function startCpuProfile(): Promise { - // Adopt session started in bin/nuxi.mjs - const cli = globalThis.__nuxt_cli__ as typeof globalThis.__nuxt_cli__ & { cpuProfileSession?: import('node:inspector').Session } + const cli = globalThis.__nuxt_cli__ as Record | undefined if (cli?.cpuProfileSession) { session = cli.cpuProfileSession delete cli.cpuProfileSession @@ -26,38 +23,15 @@ export async function startCpuProfile(): Promise { if (err) { rej(err) } - else { res() } + else { + res() + } }) }) }) } -export function stopCpuProfile(outDir: string): Promise { - if (!session) { - return Promise.resolve(undefined) - } - const s = session - session = undefined - return new Promise((res, rej) => { - s.post('Profiler.stop', (err, { profile }) => { - if (err) { - return rej(err) - } - const outPath = join(outDir, `profile-${profileCount++}.cpuprofile`) - mkdir(outDir, { recursive: true }) - .then(() => writeFile(outPath, JSON.stringify(profile))) - .then(() => { - logger.info(`CPU profile written to ${colors.cyan(outPath)}`) - logger.info(`Open it in ${colors.cyan('https://www.speedscope.app')} or Chrome DevTools`) - s.disconnect() - res(outPath) - }) - .catch(rej) - }) - }) -} - -function stopCpuProfileSync(outDir: string): string | undefined { +export function stopCpuProfile(outDir: string): string | undefined { if (!session) { return } @@ -80,20 +54,3 @@ function stopCpuProfileSync(outDir: string): string | undefined { }) return outPath } - -/** - * Install signal handlers that flush the CPU profile before exit. - * Returns a cleanup function to remove the handlers. - */ -export function installSignalHandlers(outDir: string): () => void { - const onSignal = (signal: NodeJS.Signals) => { - stopCpuProfileSync(outDir) - process.kill(process.pid, signal) - } - process.once('SIGINT', onSignal) - process.once('SIGTERM', onSignal) - return () => { - process.off('SIGINT', onSignal) - process.off('SIGTERM', onSignal) - } -} From 26b08e5a2a7d185212578836736e9dbbbd7cd8a6 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 10 Mar 2026 06:24:40 +0000 Subject: [PATCH 08/13] fix: handle session cleanup --- packages/nuxi/src/commands/build.ts | 117 +++++++++++++++------------- packages/nuxi/src/utils/profile.ts | 65 ++++++++++------ packages/nuxt-cli/bin/nuxi.mjs | 27 +++++-- 3 files changed, 124 insertions(+), 85 deletions(-) diff --git a/packages/nuxi/src/commands/build.ts b/packages/nuxi/src/commands/build.ts index bdf92dbaa..87798c551 100644 --- a/packages/nuxi/src/commands/build.ts +++ b/packages/nuxi/src/commands/build.ts @@ -46,76 +46,81 @@ export default defineCommand({ await startCpuProfile() } - intro(colors.cyan('Building Nuxt for production...')) + try { + intro(colors.cyan('Building Nuxt for production...')) - const kit = await loadKit(cwd) + const kit = await loadKit(cwd) - await showVersions(cwd, kit, ctx.args.dotenv) + await showVersions(cwd, kit, ctx.args.dotenv) - const nuxt = await kit.loadNuxt({ - cwd, - dotenv: { + const nuxt = await kit.loadNuxt({ cwd, - fileName: ctx.args.dotenv, - }, - envName: ctx.args.envName, // c12 will fall back to NODE_ENV - overrides: { - logLevel: ctx.args.logLevel as 'silent' | 'info' | 'verbose', - // TODO: remove in 3.8 - _generate: ctx.args.prerender, - nitro: { - static: ctx.args.prerender, - preset: ctx.args.preset || process.env.NITRO_PRESET || process.env.SERVER_PRESET, + dotenv: { + cwd, + fileName: ctx.args.dotenv, }, - ...(ctx.args.extends && { extends: ctx.args.extends }), - ...((perfValue || ctx.data?.overrides?.debug) && { - debug: { - ...ctx.data?.overrides?.debug, - ...(perfValue && { perf: perfValue }), + envName: ctx.args.envName, // c12 will fall back to NODE_ENV + overrides: { + logLevel: ctx.args.logLevel as 'silent' | 'info' | 'verbose', + // TODO: remove in 3.8 + _generate: ctx.args.prerender, + nitro: { + static: ctx.args.prerender, + preset: ctx.args.preset || process.env.NITRO_PRESET || process.env.SERVER_PRESET, }, - }), - ...ctx.data?.overrides, - }, - }) - - let nitro: ReturnType | undefined - // In Bridge, if Nitro is not enabled, useNitro will throw an error - try { - // Use ? for backward compatibility for Nuxt <= RC.10 - nitro = kit.useNitro?.() - if (nitro) { - logger.info(`Nitro preset: ${colors.cyan(nitro.options.preset)}`) + ...(ctx.args.extends && { extends: ctx.args.extends }), + ...ctx.data?.overrides, + ...((perfValue || ctx.data?.overrides?.debug) && { + debug: { + ...ctx.data?.overrides?.debug, + ...(perfValue && { perf: perfValue }), + }, + }), + }, + }) + + let nitro: ReturnType | undefined + // In Bridge, if Nitro is not enabled, useNitro will throw an error + try { + // Use ? for backward compatibility for Nuxt <= RC.10 + nitro = kit.useNitro?.() + if (nitro) { + logger.info(`Nitro preset: ${colors.cyan(nitro.options.preset)}`) + } + } + catch { + // } - } - catch { - // - } - - await clearBuildDir(nuxt.options.buildDir) - await kit.writeTypes(nuxt) + await clearBuildDir(nuxt.options.buildDir) - nuxt.hook('build:error', (err) => { - logger.error(`Nuxt build error: ${err}`) - process.exit(1) - }) + await kit.writeTypes(nuxt) - await kit.buildNuxt(nuxt) + nuxt.hook('build:error', (err) => { + logger.error(`Nuxt build error: ${err}`) + process.exit(1) + }) - stopCpuProfile(cwd) + await kit.buildNuxt(nuxt) - if (ctx.args.prerender) { - if (!nuxt.options.ssr) { - logger.warn(`HTML content not prerendered because ${colors.cyan('ssr: false')} was set.`) - logger.info(`You can read more in ${colors.cyan('https://nuxt.com/docs/getting-started/deployment#static-hosting')}.`) + if (ctx.args.prerender) { + if (!nuxt.options.ssr) { + logger.warn(`HTML content not prerendered because ${colors.cyan('ssr: false')} was set.`) + logger.info(`You can read more in ${colors.cyan('https://nuxt.com/docs/getting-started/deployment#static-hosting')}.`) + } + // TODO: revisit later if/when nuxt build --prerender will output hybrid + const dir = nitro?.options.output.publicDir + const publicDir = dir ? relative(process.cwd(), dir) : '.output/public' + outro(`✨ You can now deploy ${colors.cyan(publicDir)} to any static hosting!`) + } + else { + outro('✨ Build complete!') } - // TODO: revisit later if/when nuxt build --prerender will output hybrid - const dir = nitro?.options.output.publicDir - const publicDir = dir ? relative(process.cwd(), dir) : '.output/public' - outro(`✨ You can now deploy ${colors.cyan(publicDir)} to any static hosting!`) } - else { - outro('✨ Build complete!') + finally { + if (profileArg) { + await stopCpuProfile(cwd) + } } }, }) diff --git a/packages/nuxi/src/utils/profile.ts b/packages/nuxi/src/utils/profile.ts index b9fda11e0..5ad826c02 100644 --- a/packages/nuxi/src/utils/profile.ts +++ b/packages/nuxi/src/utils/profile.ts @@ -17,40 +17,59 @@ export async function startCpuProfile(): Promise { const inspector = await import('node:inspector') session = new inspector.Session() session.connect() - await new Promise((res, rej) => { - session!.post('Profiler.enable', () => { - session!.post('Profiler.start', (err) => { + try { + await new Promise((res, rej) => { + session!.post('Profiler.enable', (err) => { if (err) { - rej(err) + return rej(err) } - else { + session!.post('Profiler.start', (err) => { + if (err) { + return rej(err) + } res() - } + }) }) }) - }) + } + catch (err) { + session.disconnect() + session = undefined + throw err + } } -export function stopCpuProfile(outDir: string): string | undefined { +export async function stopCpuProfile(outDir: string): Promise { if (!session) { return } const s = session session = undefined - let outPath: string | undefined - s.post('Profiler.stop', (_err, params) => { - if (_err || !params?.profile) { - return - } - outPath = join(outDir, `profile-${profileCount++}.cpuprofile`) - try { - mkdirSync(outDir, { recursive: true }) - writeFileSync(outPath, JSON.stringify(params.profile)) - logger.info(`CPU profile written to ${colors.cyan(outPath)}`) - logger.info(`Open it in ${colors.cyan('https://www.speedscope.app')} or Chrome DevTools`) - } - catch {} + const outPath = join(outDir, `profile-${profileCount++}.cpuprofile`) + try { + await new Promise((resolve, reject) => { + s.post('Profiler.stop', (err, params) => { + if (err) { + return reject(err) + } + + if (!params?.profile) { + return resolve(params) + } + + try { + mkdirSync(outDir, { recursive: true }) + writeFileSync(outPath, JSON.stringify(params.profile)) + logger.info(`CPU profile written to ${colors.cyan(outPath)}`) + logger.info(`Open it in ${colors.cyan('https://www.speedscope.app')} or Chrome DevTools`) + } + catch {} + + resolve(params) + }) + }) + } + finally { s.disconnect() - }) - return outPath + } } diff --git a/packages/nuxt-cli/bin/nuxi.mjs b/packages/nuxt-cli/bin/nuxi.mjs index 259422031..7721e99cd 100755 --- a/packages/nuxt-cli/bin/nuxi.mjs +++ b/packages/nuxt-cli/bin/nuxi.mjs @@ -32,13 +32,28 @@ if ( ) { const session = new inspector.Session() session.connect() - // eslint-disable-next-line antfu/no-top-level-await - await new Promise((resolve) => { - session.post('Profiler.enable', () => { - session.post('Profiler.start', resolve) + + try { + // eslint-disable-next-line antfu/no-top-level-await + await new Promise((resolve, reject) => { + session.post('Profiler.enable', (err) => { + if (err) { + return reject(err) + } + session.post('Profiler.start', (err) => { + if (err) { + return reject(err) + } + resolve() + }) + }) }) - }) - globalThis.__nuxt_cli__.cpuProfileSession = session + globalThis.__nuxt_cli__.cpuProfileSession = session + } + catch (err) { + session.disconnect() + throw err + } } // eslint-disable-next-line antfu/no-top-level-await From b6dc05320af6406b23d9f47e2aa343f72c22f3d7 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 10 Mar 2026 06:36:26 +0000 Subject: [PATCH 09/13] fix: improve types for `__nuxt_cli__` --- packages/nuxi/bin/nuxi.mjs | 1 + packages/nuxi/src/utils/profile.ts | 2 +- types.d.ts | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/nuxi/bin/nuxi.mjs b/packages/nuxi/bin/nuxi.mjs index 259422031..49df74bd9 100755 --- a/packages/nuxi/bin/nuxi.mjs +++ b/packages/nuxi/bin/nuxi.mjs @@ -24,6 +24,7 @@ globalThis.__nuxt_cli__ = { startTime: Date.now(), entry: fileURLToPath(import.meta.url), devEntry: fileURLToPath(new URL('../dist/dev/index.mjs', import.meta.url)), + cpuProfileSession: undefined, } if ( diff --git a/packages/nuxi/src/utils/profile.ts b/packages/nuxi/src/utils/profile.ts index 5ad826c02..d73159fe6 100644 --- a/packages/nuxi/src/utils/profile.ts +++ b/packages/nuxi/src/utils/profile.ts @@ -8,7 +8,7 @@ let session: Session | undefined let profileCount = 0 export async function startCpuProfile(): Promise { - const cli = globalThis.__nuxt_cli__ as Record | undefined + const cli = globalThis.__nuxt_cli__ if (cli?.cpuProfileSession) { session = cli.cpuProfileSession delete cli.cpuProfileSession diff --git a/types.d.ts b/types.d.ts index 6ac85db41..b88e543e1 100644 --- a/types.d.ts +++ b/types.d.ts @@ -1,3 +1,5 @@ +import type { Session } from 'node:inspector' + declare global { // eslint-disable-next-line vars-on-top var __nuxt_cli__: @@ -6,7 +8,7 @@ declare global { entry: string devEntry?: string startTime: number - cpuProfileSession?: import('node:inspector').Session + cpuProfileSession?: Session } } From 4acc72c7b821f38c4086b2d018a13bf71f51003e Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 10 Mar 2026 06:37:04 +0000 Subject: [PATCH 10/13] fix: stop cpu profile before calling process.exit --- packages/nuxi/src/commands/build.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/nuxi/src/commands/build.ts b/packages/nuxi/src/commands/build.ts index 87798c551..3ad1b172b 100644 --- a/packages/nuxi/src/commands/build.ts +++ b/packages/nuxi/src/commands/build.ts @@ -98,6 +98,9 @@ export default defineCommand({ nuxt.hook('build:error', (err) => { logger.error(`Nuxt build error: ${err}`) + if (profileArg) { + stopCpuProfile(cwd) + } process.exit(1) }) From 0317fb95da5fda181665560ad700bb73ce18c34e Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 10 Mar 2026 10:55:50 +0100 Subject: [PATCH 11/13] fix: improve logging + await profiler when exiting intentionally --- packages/nuxi/src/commands/build.ts | 6 +++--- packages/nuxi/src/dev/index.ts | 2 +- packages/nuxi/src/utils/profile.ts | 13 ++++++++----- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/nuxi/src/commands/build.ts b/packages/nuxi/src/commands/build.ts index 3ad1b172b..818139d3f 100644 --- a/packages/nuxi/src/commands/build.ts +++ b/packages/nuxi/src/commands/build.ts @@ -96,10 +96,10 @@ export default defineCommand({ await kit.writeTypes(nuxt) - nuxt.hook('build:error', (err) => { + nuxt.hook('build:error', async (err) => { logger.error(`Nuxt build error: ${err}`) if (profileArg) { - stopCpuProfile(cwd) + await stopCpuProfile(cwd, 'build') } process.exit(1) }) @@ -122,7 +122,7 @@ export default defineCommand({ } finally { if (profileArg) { - await stopCpuProfile(cwd) + await stopCpuProfile(cwd, 'build') } } }, diff --git a/packages/nuxi/src/dev/index.ts b/packages/nuxi/src/dev/index.ts index dbeb02279..5a2a183d9 100644 --- a/packages/nuxi/src/dev/index.ts +++ b/packages/nuxi/src/dev/index.ts @@ -120,7 +120,7 @@ export async function initialize(devContext: NuxtDevContext, ctx: InitializeOpti } if (profileArg) { - process.once('exit', () => stopCpuProfile(devContext.cwd)) + process.once('exit', () => stopCpuProfile(devContext.cwd, 'dev')) } return { diff --git a/packages/nuxi/src/utils/profile.ts b/packages/nuxi/src/utils/profile.ts index d73159fe6..d02e39cfd 100644 --- a/packages/nuxi/src/utils/profile.ts +++ b/packages/nuxi/src/utils/profile.ts @@ -1,7 +1,8 @@ import type { Session } from 'node:inspector' import { mkdirSync, writeFileSync } from 'node:fs' +import process from 'node:process' import { colors } from 'consola/utils' -import { join } from 'pathe' +import { join, relative } from 'pathe' import { logger } from './logger' let session: Session | undefined @@ -39,13 +40,15 @@ export async function startCpuProfile(): Promise { } } -export async function stopCpuProfile(outDir: string): Promise { +export async function stopCpuProfile(outDir: string, command: string): Promise { if (!session) { return } const s = session session = undefined - const outPath = join(outDir, `profile-${profileCount++}.cpuprofile`) + const count = profileCount++ + const outPath = join(outDir, `nuxt-${command}${count ? `-${count}` : ''}.cpuprofile`) + const relativeOutPath = relative(process.cwd(), outPath).replace(/^(?![^.]{1,2}\/)/, './') try { await new Promise((resolve, reject) => { s.post('Profiler.stop', (err, params) => { @@ -60,8 +63,8 @@ export async function stopCpuProfile(outDir: string): Promise Date: Tue, 10 Mar 2026 11:02:38 +0100 Subject: [PATCH 12/13] feat: use box --- packages/nuxi/src/utils/profile.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/nuxi/src/utils/profile.ts b/packages/nuxi/src/utils/profile.ts index d02e39cfd..a076948d1 100644 --- a/packages/nuxi/src/utils/profile.ts +++ b/packages/nuxi/src/utils/profile.ts @@ -1,9 +1,10 @@ import type { Session } from 'node:inspector' import { mkdirSync, writeFileSync } from 'node:fs' import process from 'node:process' +import { box } from '@clack/prompts' import { colors } from 'consola/utils' import { join, relative } from 'pathe' -import { logger } from './logger' +import { themeColor } from './ascii' let session: Session | undefined let profileCount = 0 @@ -63,8 +64,20 @@ export async function stopCpuProfile(outDir: string, command: string): Promise ` › ${step}`).join('\n')}\n`, '', { + contentAlign: 'left', + titleAlign: 'left', + width: 'auto', + titlePadding: 2, + contentPadding: 2, + rounded: true, + withGuide: false, + formatBorder: (text: string) => `${themeColor + text}\x1B[0m`, + }) } catch {} From 61d3b9390f680ab507b72e431e7542de40909052 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 10 Mar 2026 11:22:42 +0100 Subject: [PATCH 13/13] fix: disable forking when profiling --- packages/nuxi/src/commands/dev.ts | 3 ++- packages/nuxi/src/dev/index.ts | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/nuxi/src/commands/dev.ts b/packages/nuxi/src/commands/dev.ts index c8fe5db54..3a1518006 100644 --- a/packages/nuxi/src/commands/dev.ts +++ b/packages/nuxi/src/commands/dev.ts @@ -85,7 +85,8 @@ const command = defineCommand({ showBanner: true, }) - if (!ctx.args.fork) { + // Disable forking when profiling to capture all activity in one process + if (!ctx.args.fork || ctx.args.profile) { return { listener, close, diff --git a/packages/nuxi/src/dev/index.ts b/packages/nuxi/src/dev/index.ts index 5a2a183d9..c000c2fd0 100644 --- a/packages/nuxi/src/dev/index.ts +++ b/packages/nuxi/src/dev/index.ts @@ -120,7 +120,14 @@ export async function initialize(devContext: NuxtDevContext, ctx: InitializeOpti } if (profileArg) { - process.once('exit', () => stopCpuProfile(devContext.cwd, 'dev')) + for (const signal of [ + 'exit', + 'SIGTERM' /* Graceful shutdown */, + 'SIGINT' /* Ctrl-C */, + 'SIGQUIT' /* Ctrl-\ */, + ] as const) { + process.once(signal, () => stopCpuProfile(devContext.cwd, 'dev')) + } } return {