Skip to content

feat(kit,nitro,nuxt,schema,vite): add build profiling#34468

Merged
danielroe merged 10 commits intomainfrom
feat/perf
Mar 10, 2026
Merged

feat(kit,nitro,nuxt,schema,vite): add build profiling#34468
danielroe merged 10 commits intomainfrom
feat/perf

Conversation

@danielroe
Copy link
Copy Markdown
Member

@danielroe danielroe commented Mar 8, 2026

🔗 Linked issue

📚 Description

you can test with:

NUXT_DEBUG_PERF=1 pnpm nuxt build
this produces a report that looks like this after a build:
                                                                                                                12:15:38 PM
 Nuxt Build Performance                                                                                         12:15:38 PM
                                                                                                                12:15:38 PM
  Phase                       Duration       RSS Delta      Heap Delta                                          12:15:38 PM
  ────────────────────────────────────────────────────────────────────                                          12:15:38 PM
  config                           7ms       +144.0 KB         +3.5 MB  █                                       12:15:38 PM
  init                             2ms       +208.0 KB       +759.1 KB  █                                       12:15:38 PM
  modules                         37ms         +3.1 MB        -12.6 MB  █                                       12:15:38 PM
  modules:done                    17ms         +9.7 MB        -13.6 MB  █                                       12:15:38 PM
  nitro:config                   144ms        +20.8 MB         -2.3 MB  █                                       12:15:38 PM
  nitro:createNitro              452ms        -25.1 MB         -7.4 MB  █                                       12:15:38 PM
  ready                            2ms        +48.0 KB       +126.1 KB  █                                       12:15:38 PM
  prepare:types                  185ms      +1008.0 KB        +14.9 MB  █                                       12:15:38 PM
  app:generate                   271ms        -39.4 MB        -26.6 MB  █                                       12:15:38 PM
  app:resolve                      6ms       -464.0 KB       +270.7 KB  █                                       12:15:38 PM
  app:templates                  114ms        -21.3 MB         +8.8 MB  █                                       12:15:38 PM
  app:templatesGenerated         101ms        +43.8 MB        +17.0 MB  █                                       12:15:38 PM
  build:bundle                    3.3s        -17.6 MB        +20.4 MB  ███                                     12:15:38 PM
  build:done                       1ms       +256.0 KB        +44.0 KB  █                                       12:15:38 PM
  nitro:build                    13.0s       +455.4 MB       +215.6 MB  ████████████                            12:15:38 PM
  ────────────────────────────────────────────────────────────────────                                          12:15:38 PM
  Total                          27.0s       +948.2 MB       +655.4 MB                                          12:15:38 PM
                                                                                                                12:15:38 PM
 Modules                                                                                                        12:15:38 PM
                                                                                                                12:15:38 PM
  • @nuxt/devtools — 101ms                                                                                      12:15:38 PM
  • nuxt:pages — 46ms                                                                                           12:15:38 PM
                                                                                                                12:15:38 PM
 Bundler Plugins                                                                                                12:15:38 PM
                                                                                                                12:15:38 PM
  • vite:load-fallback load — 239ms/file, max 749ms (1193 calls)                                                12:15:38 PM
  • vite:css-post transform — 119ms/file, max 598ms (60 calls)                                                  12:15:38 PM
  • vite:esbuild transform — 119ms/file, max 1.1s (1726 calls)                                                  12:15:38 PM
  • vite:vue transform — 106ms/file, max 1.0s (1726 calls)                                                      12:15:38 PM
  • vite:define transform — 32ms/file, max 653ms (1726 calls)                                                   12:15:38 PM
  • vite:vue-jsx transform — 32ms/file, max 156ms (6 calls)                                                     12:15:38 PM
  • vite:css transform — 16ms/file, max 325ms (60 calls)                                                        12:15:38 PM
  • nuxt:components:imports-alias transform — 5ms/file, max 32ms (10 calls)                                     12:15:38 PM
  • nuxt:imports-transform transform — 3ms/file, max 39ms (2561 calls)                                          12:15:38 PM
  • nuxt:prehydrate-transform transform — 3ms/file, max 23ms (12 calls)                                         12:15:38 PM
  • nuxt:compiler:keyed-functions transform — 2ms/file, max 27ms (172 calls)                                    12:15:38 PM
  • nuxt:resolve-bare-imports resolveId — 1ms/file, max 39ms (72 calls)                                         12:15:38 PM
  • nuxt:tree-shake-composables:transform transform — 1ms/file, max 48ms (138 calls)                            12:15:38 PM
  • nuxt:lazy-hydration-macro transform — 710µs/file, max 2ms (18 calls)                                        12:15:38 PM
  • vite:resolve resolveId — 620µs/file, max 8ms (4044 calls)                                                   12:15:38 PM
                                                                                                                12:15:38 PM
 Details                                                                                                        12:15:38 PM
                                                                                                                12:15:38 PM
  • module:@nuxt/devtools — 101ms                                                                               12:15:38 PM
  • module:nuxt:pages — 46ms                                                                                    12:15:38 PM
  • vite:extend — 13ms                                                                                          12:15:38 PM
  • vite:client — 4.8s                                                                                          12:15:38 PM
  • vite:server — 3.5s                                                                                          12:15:38 PM
                                                                                                                12:15:38 PM
[12:15:38 PM]  Full report written to /home/daniel/code/nuxt/nuxt/test/fixtures/basic/node_modules/.cache/nuxt/.nuxt/perf-report.json
                                                                                                                12:15:38 PM
│                                                                                                               12:15:38 PM
└  ✨ Build complete!
SCR-20260308-rxba

🚧 TODO

  • integrate with vite's profiler we launch our own cpu profiler
  • enable --profile flag in nuxt/cli

@bolt-new-by-stackblitz
Copy link
Copy Markdown

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 8, 2026

Open in StackBlitz

@nuxt/kit

npm i https://pkg.pr.new/@nuxt/kit@34468

@nuxt/nitro-server

npm i https://pkg.pr.new/@nuxt/nitro-server@34468

nuxt

npm i https://pkg.pr.new/nuxt@34468

@nuxt/rspack-builder

npm i https://pkg.pr.new/@nuxt/rspack-builder@34468

@nuxt/schema

npm i https://pkg.pr.new/@nuxt/schema@34468

@nuxt/vite-builder

npm i https://pkg.pr.new/@nuxt/vite-builder@34468

@nuxt/webpack-builder

npm i https://pkg.pr.new/@nuxt/webpack-builder@34468

commit: 5e87c8c

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Mar 8, 2026

Merging this PR will improve performance by 11.18%

⚡ 1 improved benchmark
✅ 19 untouched benchmarks
⏩ 3 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
loadNuxt in the basic test fixture 430.4 ms 387.2 ms +11.18%

Comparing feat/perf (a39c43c) with main (1dd52ba)2

Open in CodSpeed

Footnotes

  1. 3 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (9d27cf5) during the generation of this report, so 1dd52ba was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@danielroe danielroe marked this pull request as ready for review March 9, 2026 17:55
@danielroe danielroe enabled auto-merge March 9, 2026 17:55
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 9, 2026

Walkthrough

Adds in-process performance profiling across Nuxt: CLI docs for build, dev and generate include a new --profile[=verbose] flag. Introduces NuxtPerfProfiler class and attaches it to the Nuxt instance as an optional _perf API. Integrates phase timing around init, modules, app generation, bundling, Nitro build/dev flows and module setup. Instruments Vite plugins via a new PerfPlugin and wraps Rollup/Nitro hooks for per-hook timing. Adds schema/types support (debug.perf, NUXT_DEBUG_PERF) and export surface for perf methods, plus report/trace generation and lifecycle teardown.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main feature added: build profiling functionality across multiple packages (kit, nitro, nuxt, schema, vite).
Description check ✅ Passed The description clearly relates to the changeset by explaining how to use the new profiling feature (NUXT_DEBUG_PERF=1) and showing example output, matching the profiling implementation in the changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/perf

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/schema/src/config/common.ts (1)

121-134: ⚠️ Potential issue | 🟠 Major

Don’t make debug: true opt in to profiling.

This branch now evaluates perf to true whenever NUXT_DEBUG_PERF is unset, so every existing debug: true project will start CPU profiling and emit perf artefacts unexpectedly. Please keep perf explicit here.

Suggested change
       if (val === true) {
         return {
           templates: true,
           modules: true,
           watchers: true,
           hooks: {
             client: true,
             server: true,
           },
           nitro: true,
           router: true,
           hydration: true,
-          perf: process.env.NUXT_DEBUG_PERF === 'quiet' ? 'quiet' : true,
-        } satisfies Required<NuxtDebugOptions>
+          ...(process.env.NUXT_DEBUG_PERF
+            ? { perf: process.env.NUXT_DEBUG_PERF === 'quiet' ? 'quiet' : true }
+            : {}),
+        } satisfies NuxtDebugOptions
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/schema/src/config/common.ts` around lines 121 - 134, The branch that
handles val === true currently sets perf to true when NUXT_DEBUG_PERF is unset,
enabling profiling by default; change it so perf is false unless NUXT_DEBUG_PERF
is explicitly set (and 'quiet' maps to 'quiet'), e.g. make the perf value
conditional on process.env.NUXT_DEBUG_PERF so it remains false by default and
only enables profiling when the env var is present (and preserves the 'quiet'
mapping); update the object returned in the val === true branch (the object
annotated with satisfies Required<NuxtDebugOptions>) to implement this behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/kit/src/module/define.ts`:
- Around line 125-131: Wrap the module.setup invocation and timing in a
try/finally so nuxt._perf.endPhase(`module:${moduleName}`) always runs even if
module.setup throws: call nuxt._perf?.startPhase before invoking module.setup
(as already done), record start = performance.now(), then in a finally block
compute perf = performance.now() - start, set setupTime
(Math.round((perf*100))/100), call nuxt._perf?.endPhase(`module:${moduleName}`),
and rethrow any caught error so behavior is unchanged; update the code around
module.setup?.call(...) in define.ts to use this try/finally pattern.

In `@packages/nuxt/src/core/nuxt.ts`:
- Around line 943-955: The onSignal handler currently calls process.exit(0)
which masks interrupt failures; change it so after performing the flush/dispose
steps it preserves the original signal semantics by re-emitting the received
signal (e.g. process.kill(process.pid, signal)) or at minimum exits with a
non-zero code instead of 0; update the registration of the handlers
(process.once('SIGINT', onSignal) / process.once('SIGTERM', onSignal)) and
modify onSignal to accept the signal name (e.g. onSignal = (signal) => { ... })
so you can re-raise that same signal after flushing (or call process.exit(1) if
re-emitting is not feasible), and remove the unconditional process.exit(0) call.
- Around line 807-823: The profiler may be created too late because
options.debug.perf is only set after loadNuxtConfig; update the early-init logic
(around NuxtPerfProfiler, perf, cliStartTime and opts.overrides) to also check
the NUXT_DEBUG_PERF environment variable before calling loadNuxtConfig and start
the 'config' phase when true so the config loading is profiled consistently;
ensure the same code path that currently handles opts.overrides.debug.perf also
respects process.env.NUXT_DEBUG_PERF (and still falls back to options.debug.perf
after loadNuxtConfig), then keep perf?.endPhase('config') as-is.

In `@packages/nuxt/src/core/perf.ts`:
- Around line 94-95: Add a Biome-specific suppression comment immediately above
the ANSI_RE regex declaration to disable
lint/suspicious/noControlCharactersInRegex for this line; keep the existing
ESLint disable comment and insert a line like the suggested biome-ignore with a
brief justification so the ANSI_RE constant (const ANSI_RE = /\x1B\[[0-9;]*m/g)
is exempted from Biome's control-character-in-regex rule.
- Around line 173-191: stopCpuProfileSync is not actually synchronous (it uses
session.post callback) causing lost profiles on SIGINT; instead route the signal
handler through the existing async stopCpuProfile so we await the profiler stop
before calling process.exit. Update the SIGINT handler to call and await
stopCpuProfile() (not stopCpuProfileSync), ensure stopCpuProfile correctly uses
this.#cpuProfileSession and returns the output path after writing the file, and
remove or mark stopCpuProfileSync as unsafe/unused to avoid future misuse.

In `@packages/vite/src/plugins/perf.ts`:
- Around line 42-52: timedCall currently skips calling
nuxt._perf.recordBundlerPluginHook when the hook throws or returns a rejected
promise; wrap the synchronous invocation in a try/finally and ensure the async
path uses Promise.resolve(result).finally(...) so
recordBundlerPluginHook(pluginName, hookName, elapsed) always runs regardless of
success or failure, keeping the same performance measurement logic (use
performance.now() at start and compute delta in finally).

---

Outside diff comments:
In `@packages/schema/src/config/common.ts`:
- Around line 121-134: The branch that handles val === true currently sets perf
to true when NUXT_DEBUG_PERF is unset, enabling profiling by default; change it
so perf is false unless NUXT_DEBUG_PERF is explicitly set (and 'quiet' maps to
'quiet'), e.g. make the perf value conditional on process.env.NUXT_DEBUG_PERF so
it remains false by default and only enables profiling when the env var is
present (and preserves the 'quiet' mapping); update the object returned in the
val === true branch (the object annotated with satisfies
Required<NuxtDebugOptions>) to implement this behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: eefcb715-21c4-4868-b65f-d957553ea22a

📥 Commits

Reviewing files that changed from the base of the PR and between fea006d and 85a7b52.

📒 Files selected for processing (14)
  • docs/4.api/4.commands/build.md
  • docs/4.api/4.commands/dev.md
  • docs/4.api/4.commands/generate.md
  • packages/kit/src/module/define.ts
  • packages/nitro-server/src/index.ts
  • packages/nuxt/src/core/app.ts
  • packages/nuxt/src/core/builder.ts
  • packages/nuxt/src/core/nuxt.ts
  • packages/nuxt/src/core/perf.ts
  • packages/schema/src/config/common.ts
  • packages/schema/src/types/debug.ts
  • packages/schema/src/types/nuxt.ts
  • packages/vite/src/plugins/perf.ts
  • packages/vite/src/vite.ts

Comment on lines +807 to +823
// Early-init profiler when CLI passes perf overrides (captures config loading).
// Otherwise, create after config resolves from nuxt.config / env vars.
let perf: NuxtPerfProfiler | undefined
const cliStartTime = (globalThis as any).__nuxt_cli__?.startTime as number | undefined
const perfOverride = typeof opts.overrides?.debug === 'object' && opts.overrides.debug.perf
if (perfOverride) {
perf = new NuxtPerfProfiler({ startTime: cliStartTime })
perf.startPhase('config')
}

const options = await loadNuxtConfig(opts)

if (!perf && typeof options.debug === 'object' && options.debug.perf) {
perf = new NuxtPerfProfiler({ startTime: cliStartTime })
}
perf?.endPhase('config')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Initialise the profiler before loadNuxtConfig() for the env-var flow.

NUXT_DEBUG_PERF=1 only becomes visible in options.debug.perf after config resolution, so this branch creates perf too late and the config phase never starts in the main workflow. Check the env var here as well so config loading is profiled consistently.

Suggested change
   let perf: NuxtPerfProfiler | undefined
   const cliStartTime = (globalThis as any).__nuxt_cli__?.startTime as number | undefined
   const perfOverride = typeof opts.overrides?.debug === 'object' && opts.overrides.debug.perf
-  if (perfOverride) {
+  const perfFromEnv = process.env.NUXT_DEBUG_PERF
+  if (perfOverride || perfFromEnv) {
     perf = new NuxtPerfProfiler({ startTime: cliStartTime })
     perf.startPhase('config')
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/core/nuxt.ts` around lines 807 - 823, The profiler may be
created too late because options.debug.perf is only set after loadNuxtConfig;
update the early-init logic (around NuxtPerfProfiler, perf, cliStartTime and
opts.overrides) to also check the NUXT_DEBUG_PERF environment variable before
calling loadNuxtConfig and start the 'config' phase when true so the config
loading is profiled consistently; ensure the same code path that currently
handles opts.overrides.debug.perf also respects process.env.NUXT_DEBUG_PERF (and
still falls back to options.debug.perf after loadNuxtConfig), then keep
perf?.endPhase('config') as-is.

Comment on lines +943 to +955
// Signal handlers must be synchronous
const onSignal = () => {
if (!flushed) {
flushed = true
if (!quiet) { perf!.printReport({ title }) }
perf!.writeReportSync(nuxt.options.buildDir, { quiet })
perf!.stopCpuProfileSync(nuxt.options.buildDir)
perf!.dispose()
}
process.exit(0)
}
process.once('SIGINT', onSignal)
process.once('SIGTERM', onSignal)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t turn SIGINT/SIGTERM into a successful exit.

This handler always calls process.exit(0), so an interrupted profiled build is reported as success to shells and CI. Please preserve the signal semantics here by re-emitting the signal after flushing, or at least by setting the conventional non-zero exit code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/core/nuxt.ts` around lines 943 - 955, The onSignal handler
currently calls process.exit(0) which masks interrupt failures; change it so
after performing the flush/dispose steps it preserves the original signal
semantics by re-emitting the received signal (e.g. process.kill(process.pid,
signal)) or at minimum exits with a non-zero code instead of 0; update the
registration of the handlers (process.once('SIGINT', onSignal) /
process.once('SIGTERM', onSignal)) and modify onSignal to accept the signal name
(e.g. onSignal = (signal) => { ... }) so you can re-raise that same signal after
flushing (or call process.exit(1) if re-emitting is not feasible), and remove
the unconditional process.exit(0) call.

Comment on lines +94 to +95
// eslint-disable-next-line no-control-regex
const ANSI_RE = /\x1B\[[0-9;]*m/g
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python - <<'PY'
from pathlib import Path

path = Path("packages/nuxt/src/core/perf.ts")
line = path.read_text(encoding="utf-8", errors="surrogatepass").splitlines()[94]
print(repr(line))
print("Contains ESC byte:", "\x1b" in line)
PY

Repository: nuxt/nuxt

Length of output: 115


🏁 Script executed:

# Check for Biome configuration files
find . -maxdepth 2 -type f \( -name "biome.json" -o -name "biome.jsonc" -o -name ".biomerc*" \) 2>/dev/null | head -20

Repository: nuxt/nuxt

Length of output: 65


🏁 Script executed:

# Check if there are any Biome suppress comments in the codebase
rg "@biome.*noControlCharactersInRegex|biome.*suppress.*regex" --type ts --type tsx -A 2 -B 2 | head -50

Repository: nuxt/nuxt

Length of output: 80


🏁 Script executed:

# Check the context around line 94-95 in perf.ts
sed -n '90,100p' packages/nuxt/src/core/perf.ts

Repository: nuxt/nuxt

Length of output: 437


🌐 Web query:

Biome linter suppress control characters in regex comment syntax

💡 Result:

To suppress Biome’s “control characters in regex” diagnostic (lint/suspicious/noControlCharactersInRegex), add a suppression comment on the line above the regex literal:

// biome-ignore lint/suspicious/noControlCharactersInRegex: needed for this pattern
const re = /\x00/;

For a larger block, use a range suppression:

// biome-ignore-start lint/suspicious/noControlCharactersInRegex: vendor regex
const a = /\x00/;
const b = /\u000C/;
// biome-ignore-end lint/suspicious/noControlCharactersInRegex: vendor regex

Sources: Biome suppression comment syntax and range suppressions [1]; rule category/name for this diagnostic [2].


Add Biome suppression comment for regex control character rule.

The ESLint suppression does not suppress Biome's lint/suspicious/noControlCharactersInRegex rule. Add a Biome-specific suppression comment above the regex:

Suggested fix
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape sequences required for stripping formatting
const ANSI_RE = /\x1B\[[0-9;]*m/g
🧰 Tools
🪛 Biome (2.4.4)

[error] 95-95: Unexpected control character in a regular expression.

(lint/suspicious/noControlCharactersInRegex)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/core/perf.ts` around lines 94 - 95, Add a Biome-specific
suppression comment immediately above the ANSI_RE regex declaration to disable
lint/suspicious/noControlCharactersInRegex for this line; keep the existing
ESLint disable comment and insert a line like the suggested biome-ignore with a
brief justification so the ANSI_RE constant (const ANSI_RE = /\x1B\[[0-9;]*m/g)
is exempted from Biome's control-character-in-regex rule.

Comment on lines +173 to +191
stopCpuProfileSync (buildDir: string): string | undefined {
const session = this.#cpuProfileSession
if (!session) { return }
this.#cpuProfileSession = undefined
let outPath: string | undefined
session.post('Profiler.stop', (_err, params) => {
if (_err || !params?.profile) { return }
outPath = join(buildDir, `profile-${this.#cpuProfileCount++}.cpuprofile`)
try {
mkdirSync(buildDir, { recursive: true })
writeFileSync(outPath, JSON.stringify(params.profile))
consola.info(`CPU profile written to ${colors.cyan(outPath)}`)
consola.info(`Open it in ${colors.cyan('https://www.speedscope.app')} or Chrome DevTools`)
} catch {
// don't throw an error if we can't write the file
}
session.disconnect()
})
return outPath
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Node.js, does inspector.Session.post('Profiler.stop', callback) complete asynchronously, and is there any synchronous API for stopping a CPU profile?

💡 Result:

Yes—session.post('Profiler.stop', callback) completes asynchronously: Session.post() sends a message to the inspector backend and the callback is invoked later, when the response is received (i.e., not synchronously during the post() call). [1]

There is no synchronous API in Node’s inspector interface for stopping (and retrieving) a CPU profile. The supported APIs are the callback form (session.post(..., callback)) and the Promise-based form (await session.post('Profiler.stop') via node:inspector/promises). [1]

Sources:
[1] Node.js Inspector docs (inspector.Session.post(...), callback notified on response; Promise-based session.post usage incl. Profiler.stop) (nodejs.org)

Citations:


🏁 Script executed:

# Search for calls to stopCpuProfileSync to understand the context
rg 'stopCpuProfileSync' packages/nuxt/src/core/

Repository: nuxt/nuxt

Length of output: 232


🏁 Script executed:

# Look for the async stopCpuProfile method and the signal path handling
rg 'stopCpuProfile|signal.*exit|process.*exit' packages/nuxt/src/core/nuxt.ts -A 2 -B 2

Repository: nuxt/nuxt

Length of output: 541


🏁 Script executed:

# Look for the onSignal function definition and its full context
rg -B 10 'process\.once.*SIGINT.*onSignal' packages/nuxt/src/core/nuxt.ts | head -50

Repository: nuxt/nuxt

Length of output: 396


🏁 Script executed:

# Find the stopCpuProfile (async) method definition to confirm it exists and is async
rg -A 15 'stopCpuProfile\s*\(' packages/nuxt/src/core/perf.ts | grep -v 'stopCpuProfileSync'

Repository: nuxt/nuxt

Length of output: 868


stopCpuProfileSync() is not actually synchronous and causes profile data loss on interrupt.

session.post() completes asynchronously via callback, so the method returns before outPath is assigned. In the SIGINT signal handler path, process.exit(0) is called immediately afterwards, meaning the async callback never completes and the profile file is never written. The async stopCpuProfile() method already exists and correctly handles this flow—route the signal handler through it via await to preserve profile data on interrupt.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/core/perf.ts` around lines 173 - 191, stopCpuProfileSync is
not actually synchronous (it uses session.post callback) causing lost profiles
on SIGINT; instead route the signal handler through the existing async
stopCpuProfile so we await the profiler stop before calling process.exit. Update
the SIGINT handler to call and await stopCpuProfile() (not stopCpuProfileSync),
ensure stopCpuProfile correctly uses this.#cpuProfileSession and returns the
output path after writing the file, and remove or mark stopCpuProfileSync as
unsafe/unused to avoid future misuse.

@danielroe danielroe added this pull request to the merge queue Mar 9, 2026
@danielroe danielroe removed this pull request from the merge queue due to a manual request Mar 9, 2026
@danielroe danielroe enabled auto-merge March 10, 2026 05:52
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/nuxt/src/core/perf.ts (1)

480-494: Silent error swallowing may hide filesystem issues.

The catch block silently discards all errors. Consider logging a debug message so users can diagnose permission or disk space issues when the report fails to write.

Suggested improvement
     } catch {
-      // don't throw an error if we can't write the file
+      // don't throw an error if we can't write the file, but log for debugging
+      consola.debug(`Failed to write perf report to ${reportPath}`)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/core/perf.ts` around lines 480 - 494, The writeReportSync
function currently swallows all errors in its catch, which hides filesystem
problems; update the catch in writeReportSync to log the caught error (including
error.message and context like reportPath and buildDir) at a debug or trace
level (e.g., via consola.debug or existing logger) so failures to
mkdirSync/writeFileSync are recorded, and preserve the current behavior of not
throwing; reference writeReportSync, reportPath, buildDir and options to include
useful context in the log.
packages/nitro-server/src/index.ts (1)

775-803: Consider extracting shared timing wrapper.

The plugin hook timing logic (lines 785-799) duplicates the timedCall pattern from packages/vite/src/plugins/perf.ts. Consider extracting this to a shared utility in the perf module to reduce duplication and ensure consistent behaviour.

Example shared utility location

The timing wrapper could be exported from packages/nuxt/src/core/perf.ts:

export function createTimedHookWrapper(
  original: (...args: any[]) => any,
  ctx: any,
  args: any[],
  record: () => void
): any {
  try {
    const result = original.apply(ctx, args)
    if (result && typeof result === 'object' && 'then' in result) {
      return (result as Promise<any>).finally(record)
    }
    record()
    return result
  } catch (err) {
    record()
    throw err
  }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nitro-server/src/index.ts` around lines 775 - 803, Extract the
duplicated timing wrapper into a shared utility and use it here: create a
function (e.g. createTimedHookWrapper or timedCall) in the perf module
(referenced by nuxt._perf usage) that accepts the original function,
this/context, args and a record callback and implements the try/await/finally
pattern currently in the inline wrapper; then replace the inline wrapper inside
the nitro.hooks.hook loop (where plugin[hookName] is reassigned for each
hookName) to call that shared utility instead of duplicating the timing logic so
both packages/vite/src/plugins/perf.ts and this file use the same
implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/nitro-server/src/index.ts`:
- Around line 775-803: Extract the duplicated timing wrapper into a shared
utility and use it here: create a function (e.g. createTimedHookWrapper or
timedCall) in the perf module (referenced by nuxt._perf usage) that accepts the
original function, this/context, args and a record callback and implements the
try/await/finally pattern currently in the inline wrapper; then replace the
inline wrapper inside the nitro.hooks.hook loop (where plugin[hookName] is
reassigned for each hookName) to call that shared utility instead of duplicating
the timing logic so both packages/vite/src/plugins/perf.ts and this file use the
same implementation.

In `@packages/nuxt/src/core/perf.ts`:
- Around line 480-494: The writeReportSync function currently swallows all
errors in its catch, which hides filesystem problems; update the catch in
writeReportSync to log the caught error (including error.message and context
like reportPath and buildDir) at a debug or trace level (e.g., via consola.debug
or existing logger) so failures to mkdirSync/writeFileSync are recorded, and
preserve the current behavior of not throwing; reference writeReportSync,
reportPath, buildDir and options to include useful context in the log.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9c2b38ed-4c0e-4a76-960d-2d893f0e4290

📥 Commits

Reviewing files that changed from the base of the PR and between 85a7b52 and e52ee21.

📒 Files selected for processing (8)
  • packages/kit/src/module/define.ts
  • packages/nitro-server/src/index.ts
  • packages/nuxt/src/core/builder.ts
  • packages/nuxt/src/core/nuxt.ts
  • packages/nuxt/src/core/perf.ts
  • packages/schema/src/config/common.ts
  • packages/schema/src/types/nuxt.ts
  • packages/vite/src/plugins/perf.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/schema/src/config/common.ts
  • packages/nuxt/src/core/builder.ts
  • packages/kit/src/module/define.ts

@danielroe danielroe added this pull request to the merge queue Mar 10, 2026
@danielroe danielroe removed this pull request from the merge queue due to a manual request Mar 10, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/nuxt/src/core/perf.ts (2)

696-696: Minor: Redundant double-rounding.

round(hp.duration - round(attributedTime)) applies round() twice. The inner round isn't necessary since attributedTime is derived from already-precise floating-point arithmetic.

Suggested fix
-      const ownDuration = round(hp.duration - round(attributedTime))
+      const ownDuration = round(hp.duration - attributedTime)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/core/perf.ts` at line 696, The calculation for ownDuration
currently double-rounds: in the expression assigning ownDuration (using
hp.duration and attributedTime) remove the inner round() around attributedTime
so you call round once on the final difference; update the assignment of
ownDuration from round(hp.duration - round(attributedTime)) to round(hp.duration
- attributedTime) to avoid redundant rounding while keeping the final rounding
via round().

659-661: Consider logging a warning on write failure.

Silently catching write errors may hide file system issues (e.g., permissions, disk full). A brief warning would help users diagnose why perf reports are missing without failing the build.

Suggested change
-  } catch {
-    // don't throw an error if we can't write the file
+  } catch (err) {
+    if (!options?.quiet) {
+      consola.warn('Failed to write performance report:', err)
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nuxt/src/core/perf.ts` around lines 659 - 661, The silent catch
around the perf report file write should log a brief warning instead of
swallowing errors; modify the catch to capture the error (e.g., catch (err)) and
emit a warning via the module's logger or console.warn that includes the error
message and the target file path (the same write operation in perf.ts where the
file is written), so users see filesystem issues (permissions, disk full)
without failing the build.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/nuxt/src/core/perf.ts`:
- Line 696: The calculation for ownDuration currently double-rounds: in the
expression assigning ownDuration (using hp.duration and attributedTime) remove
the inner round() around attributedTime so you call round once on the final
difference; update the assignment of ownDuration from round(hp.duration -
round(attributedTime)) to round(hp.duration - attributedTime) to avoid redundant
rounding while keeping the final rounding via round().
- Around line 659-661: The silent catch around the perf report file write should
log a brief warning instead of swallowing errors; modify the catch to capture
the error (e.g., catch (err)) and emit a warning via the module's logger or
console.warn that includes the error message and the target file path (the same
write operation in perf.ts where the file is written), so users see filesystem
issues (permissions, disk full) without failing the build.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 8cee29c1-197e-4847-90be-c2eab3d7326a

📥 Commits

Reviewing files that changed from the base of the PR and between e52ee21 and a39c43c.

📒 Files selected for processing (5)
  • packages/nitro-server/src/index.ts
  • packages/nuxt/src/core/nuxt.ts
  • packages/nuxt/src/core/perf.ts
  • packages/schema/src/types/nuxt.ts
  • packages/vite/src/plugins/perf.ts

@danielroe danielroe added this pull request to the merge queue Mar 10, 2026
Merged via the queue into main with commit 917f0d5 Mar 10, 2026
48 checks passed
@danielroe danielroe deleted the feat/perf branch March 10, 2026 14:58
@github-actions github-actions bot mentioned this pull request Mar 10, 2026
This was referenced Mar 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant