Skip to content

feat(build): quieter Nitro server build output - #1662

Draft
atinux wants to merge 11 commits into
mainfrom
feat/quiet-server-build
Draft

atinux wants to merge 11 commits into
mainfrom
feat/quiet-server-build

Conversation

@atinux

@atinux atinux commented Nov 8, 2026 •

Copy link
Copy Markdown
Member

Written by an AI agent on behalf of @atinux. @atinux has not reviewed it yet.

nuxt build prints the server side twice over. Vite lists every chunk of the server bundle, then Nitro prints its success lines, one line per prerendered route and a file-by-file tree of .output/server. The phase spinner (the same one nuxt dev uses) is already running the whole time, so none of that tells you anything new. On the Nitro fixture below, a piped log goes from 48 lines to 8 between Building server... and the timing breakdown, phase lines included.

Videos

Real nuxt build runs in a 110-column terminal. Before is the CLI built from main (db93607); after is this branch. Same fixture each time, with a clean .output and .nuxt before every run.

Nitro (Nuxt 4.6.4, Nitro 2.13.6: 2 pages, 1 API route, /about prerendered)

Before After
nuxt build with Nitro on main nuxt build with Nitro on this branch

@nuxt/vite-server (Nuxt 4.6.4, server: { builder: '@nuxt/vite-server' })

Before After
nuxt build with vite-server on main nuxt build with vite-server on this branch

The client file lists still print in every case. That part is #1663, which is stacked on this PR.

What changed

  • Phase names say what is bundled: Bundling client, Bundling server and Bundling nitro, shown as bundle (client), bundle (server) and bundle (nitro) in the timing breakdown: config 214ms · modules 161ms · app 460ms · types 476ms · bundle (client) 1.13s · bundle (server) 262ms · bundle (nitro) 3.28s. app stays the step that generates the app's templates, and nuxt dev already calls its bundling step bundle. The videos below were recorded before this rename.
  • Phases are timed from the build itself: a small Vite plugin moves the phase when each Vite environment starts building. That keeps the split right when a single config builds every environment, which happens with the environment API, with @nuxt/vite-server (which switches the API on during setup) and with Nuxt 5 / Nitro 3 (where the server is a nitro Vite environment built after the app).
  • Nitro output drops to warnings and errors (logLevel: 1 via nitro:config). That hides Nitro's success lines, the per-route prerender list and the file tree, which Nitro only prints when logLevel > 1.
  • Vite output for server environments is muted, while the client's file list and every warning still print. A config that builds only the server (Nuxt 4 without the environment API) gets logLevel: 'warn'. A shared config gets its Vite logger's info messages muted while a server-side environment builds.
  • One summary replaces it: Prerendered 2 routes in 1.70s when anything was prerendered, then Server (nitro) built in 2.91s · 2.32 MB (or Server (vite) … for vite-server), then Preview: npx nuxt preview, plus Nitro's deploy command when the preset has one. The size is the server output directory on disk, without source maps (vite-server writes them as *.map.json).
  • Failed prerender routes are listed by the CLI, because Nitro prints them at the level that's now hidden.
  • Prerender progress: the spinner shows a count, e.g. Prerendering /blog (12 done).
  • Progress line times are labelled: the second time is now marked as the total, e.g. Bundling nitro 2.44s · total 4.98s instead of 2.44s · 4.98s. The same reporter drives nuxt dev, so its line reads the same way.
  • Opt out: nothing changes with --log-level verbose, or when the project sets nitro.logLevel, vite.logLevel or vite.$server.logLevel. --log-level silent stays silent.

Checks

  • Real builds for each server setup. Final lines below; the Nitro and vite-server lines are from the recordings above.
    • Nuxt 4 + Nitro 2 (fixture above): Server (nitro) built in 2.91s · 2.32 MB, breakdown bundle (client) 878ms · bundle (server) 257ms · bundle (nitro) 4.66s (recorded under the earlier names).
    • Nuxt 5 nightly + Nitro 3 (playground-nightly): Server (nitro) built in 216ms · 649.21 kB, breakdown bundle (client) 525ms · bundle (server) 413ms · bundle (nitro) 220ms (recorded under the earlier names). Before this branch's latest commit, that printed 21 SSR chunk lines and the full nitro environment chunk list, and credited the whole Vite build to the Nitro phase (27ms for the client and 1.33s for Nitro).
    • Nuxt 4 + @nuxt/vite-server: Server (vite) built in 403ms · 855.65 kB, which matches the 856 kB of server chunks Vite reports with --log-level verbose.
    • --prerender (static): only Prerendered 8 routes in 1.84s prints, then the existing "deploy .output/public" outro. --log-level verbose prints the full Nitro tree, as on main.
  • tsc --noEmit, eslint and knip pass. The unit tests cover the new behaviour: muting server environments in a shared Vite config, the vite-server summary, the phase split with one shared config. The whole unit suite passes once the sandbox's TERM=dumb / NO_COLOR=1 are unset; with them, 4 dev-TUI spec files fail the same way on main.

Details and open questions

  • Banner: leaving Nitro out of the startup banner and the dev info view when another server builder is configured is split out into fix(build,dev,info): name the server builder instead of assuming Nitro #1671.

  • Vite's native progress lines: with a shared config (environment API, vite-server, Nitro 3), each server environment still prints transforming... / ✓ N modules transformed. / rendering chunks.... Vite 8's reporter writes these from Rust straight to stdout, so they can only be turned off with Vite's single global log level, which would also hide the client output. Fixing that needs a per-environment log level in Vite.

  • Still noisy, upstream in Nuxt: ℹ Building server... / ✔ Server built in 276ms come from @nuxt/vite-builder and repeat the phase line. Also, the vite-builder's hideOutput filter for the server bundle never matches in Nuxt 4: it compares messages against relative(config.root, outDir), and root is app/, so it gives ../.nuxt/dist/server. On vite-server, ✔ Server output written to .output repeats the summary.

  • Gzip size: the summary shows raw size only. Nitro gzips every server file for its total; I skipped that to keep the build fast.

  • Timing line width: the longer phase names wrap the breakdown on a 110-column terminal when every phase is present (see the Nitro video).

  • Pre-existing, unrelated: Nitro's second copy of a Rollup warning goes to stderr, which the spinner's output tap doesn't watch, so in a TTY it's written on the end of the spinner line. This also happens on main.

ELI5

When you build a Nuxt app for production, it used to print a long list of every server file it made. Now it says the server was built, how long it took and how big it is, and still shows warnings and errors. This works whether Nitro or Vite builds the server. Add --log-level verbose to get the full list back.

Drop Nitro and the Vite server bundle to warnings only during nuxt build,
list failed prerender routes ourselves, and report the server build as one
line with its time and size. --log-level verbose keeps the full output.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1ba1d-392b-7447-9cc3-9587ae9be465
@pkg-pr-new

pkg-pr-new Bot commented Nov 8, 2026 •

Copy link
Copy Markdown
  • nuxt-cli-playground

    npm i https://pkg.pr.new/create-nuxt@1662
    
    npm i https://pkg.pr.new/nuxi@1662
    
    npm i https://pkg.pr.new/@nuxt/cli@1662
    

commit: ec9e55f

@github-actions

github-actions Bot commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

CLI benchmark

@nuxt/cli v4.0.3 (baseline) vs v4.1.0-alpha.0 (this PR)

Metric baseline v4.0.3 head v4.1.0-alpha.0 Delta
nuxt --version wall time (median) 55 ms 55 ms -0.3%
nuxt --help wall time (median) 113 ms 113 ms +0.8%
nuxt dev --help wall time (median) 87 ms 86 ms -1.3%
nuxt --version modules loaded 35 35 0.0%
nuxt --version built-ins loaded 29 29 0.0%
nuxt --help modules loaded 136 136 0.0%
nuxt --help built-ins loaded 93 93 0.0%
nuxt dev --help modules loaded 64 65 +1.6%
nuxt dev --help built-ins loaded 90 90 0.0%
Installed node_modules 2.48 MB 2.56 MB +3.1%
Published tarball (packed) 245.3 kB 249.3 kB +1.6%
Full report

@nuxt/cli v4.0.3 (baseline) vs v4.1.0-alpha.0 (head)

Setting Value
Baseline ref:db93607c43b9f9a5a811ae4771730b1a97d7198f (v4.0.3)
Head local packages/nuxt-cli at df76991 (v4.1.0-alpha.0)
Node v24.22.2
OS Linux 7.0.0 (kernel 7.0.0-1017-azure)
CPU AMD EPYC 9V74 80-Core Processor x 4
Memory 15.6 GB
Load average at start 1.35, 0.43, 0.15
Run started 2026-11-10T11:49:38.117Z

Cold CLI startup

Median of 15 interleaved runs per command, one warmup discarded.

Command baseline v4.0.3 median head v4.1.0-alpha.0 median Delta baseline v4.0.3 min / p95 head v4.1.0-alpha.0 min / p95
nuxt --version 55 ms 55 ms -0.3% 53 ms / 59 ms 52 ms / 57 ms
nuxt --version (first output byte) 52 ms 51 ms -0.7% 48 ms / 55 ms 48 ms / 53 ms
nuxt --help 113 ms 113 ms +0.8% 107 ms / 116 ms 110 ms / 116 ms
nuxt --help (first output byte) 107 ms 108 ms +1.3% 102 ms / 110 ms 105 ms / 111 ms
nuxt dev --help 87 ms 86 ms -1.3% 84 ms / 113 ms 82 ms / 94 ms
nuxt dev --help (first output byte) 82 ms 81 ms -1.0% 79 ms / 108 ms 77 ms / 89 ms
nuxt <unknown-command> (no-op) 120 ms 121 ms +0.2% 115 ms / 124 ms 115 ms / 124 ms
nuxt <unknown-command> (no-op) (first output byte) 114 ms 115 ms +0.6% 109 ms / 118 ms 109 ms / 118 ms

Module load cost

Counted with a module.registerHooks load hook, compile cache disabled. Counts every JS module actually evaluated on that code path (native addons excluded). Built-ins loaded after bootstrap are counted separately, including the internal modules they load.

Command baseline v4.0.3 modules head v4.1.0-alpha.0 modules Delta baseline v4.0.3 source bytes head v4.1.0-alpha.0 source bytes Delta baseline v4.0.3 built-ins head v4.1.0-alpha.0 built-ins Delta
nuxt --version 35 35 0.0% 301.2 kB 301.2 kB +0.0% 29 29 0.0%
nuxt --help 136 136 0.0% 861.0 kB 872.6 kB +1.3% 93 93 0.0%
nuxt dev --help 64 65 +1.6% 460.8 kB 463.9 kB +0.7% 90 90 0.0%

Install footprint and published tarball

Each version installed on its own into an empty project with nothing but @nuxt/cli as a dependency, so the tree is exactly the CLI and its transitive dependencies. npm cache is warm and the registry is only consulted for metadata, so install wall time is indicative, not a network benchmark.

Metric baseline v4.0.3 head v4.1.0-alpha.0 Delta
Direct dependencies of @nuxt/cli 23 23 0.0%
Packages in the installed tree (unique name@version) 38 38 0.0%
Unique package names 38 38 0.0%
Package directories on disk (cross-check) 32 32 0.0%
Installed node_modules on disk 2.48 MB 2.56 MB +3.1%
Installed files 439 442 +0.7%
Install wall time (warm npm cache, median of 3) 1.09 s 1.08 s -0.7%
Published tarball (packed) 245.3 kB 249.3 kB +1.6%
Published tarball (unpacked) 793.7 kB 806.3 kB +1.6%
Files in tarball 104 104 0.0%

Interleaved runs on a shared runner: trust the deltas, not the absolute timings. The dev, restart and build suites run locally via pnpm bench:cli.

@codspeed

codspeed Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

1 warning

⚠️ 2 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

✅ 2 untouched benchmarks


Comparing feat/quiet-server-build (ec9e55f) with main (fa3c6fb)

Open in CodSpeed

Server builders such as @nuxt/vite-server switch on the environment API
during setup, so the client/server split can no longer be decided when the
CLI attaches. A small Vite plugin now reports each environment as it starts
building, which also splits the environment API build into app (client)
and app (server).

Amp-Thread-ID: https://ampcode.com/threads/T-01a1ba1d-392b-7447-9cc3-9587ae9be465
Nitro 3 and @nuxt/vite-server build the server as a Vite environment next
to the app. A Vite plugin now mutes Vite's info output while a server
environment builds, times the server build from its environment, and
reports the vite-server output as Server (vite). The banner leaves Nitro
out when it is not the server builder, and the preview hint points at
nuxt preview, which works for every builder.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1ba1d-392b-7447-9cc3-9587ae9be465
atinux and others added 3 commits November 10, 2026 11:34
…) and bundle (nitro)

app (client) and app (server) read as part of the app phase that
generates templates. The three phases now say what they bundle, which
also matches the bundle phase nuxt dev already reports.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1ba1d-392b-7447-9cc3-9587ae9be465
The progress line showed the phase's time and the command's next to each
other with nothing to tell them apart. The second now reads 'total', as
in 'Bundling nitro 2.44s · total 4.98s'.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1ba1d-392b-7447-9cc3-9587ae9be465

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant