Tooling, CI/CD & DX

Vite and the build

How Nuxt builds with Vite 8 and Rolldown — dev server versus production, the client and server builds, Nitro's second bundling step, chunking and manual chunks, analysing bundles, environment variables at build versus runtime, source maps, build performance and the migration from Vite 7.

Nuxt hides most of the build, which is why a senior needs to know what it does: two Vite builds, one Nitro bundling step, and a set of defaults that are right until they are not. The under the hood section covers the pipeline in detail; this page focuses on the tooling knobs you turn in practice.

Know

  • Vite 8 (March 2026) runs on Rolldown, a Rust bundler that replaced esbuild and Rollup: one engine for dev pre-bundling and production, Oxc for transforms and minification, faster builds and smaller memory. Vite 7 (June 2025) had made Node 20.19+ and ESM-only the baseline; Vite 8 keeps the Node 20.19+ / 22.12+ requirement, renames build.rollupOptions to build.rolldownOptions, and Nuxt 4.5 made it the default builder (Nuxt 4.4 was still on Vite 7.3).
  • Dev: native ESM served on demand, dependency pre-bundling, HMR through the Vue plugin; Nuxt adds the server-side render in dev via Nitro's dev server and the DevTools. nuxt dev --host, --https, --tunnel, --qr for device testing; --profile for startup profiling.
  • Production: Nuxt runs a client build (.output/public/_nuxt/* with hashed filenames and a manifest) and a server build (the SSR bundle in .nuxt/dist/server), then Nitro bundles the server with Rollup (Nitro 2) into .output/server for the chosen preset, including the renderer and your server/ routes.
  • Chunks: route-based code splitting is automatic (each page is a chunk), plus dynamic imports and lazy components; you influence grouping through vite.build.rolldownOptions.output.codeSplitting (Rolldown's replacement for manualChunks, whose object form Vite 8 removed) and keep entry hashes stable with experimental.entryImportMap. nuxt analyze runs rollup-plugin-visualizer for client and server and prints the largest modules.
  • Environment variables: anything read at build time via import.meta.env or process.env in nuxt.config.ts is baked in; anything the running server should read goes through runtimeConfig and the NUXT_* overrides. .env is loaded by nuxt dev/nuxt build (and nuxt preview, with --dotenv), never by the production server, which gets real environment variables.
  • Source maps: sourcemap: { server: true, client: 'hidden' } gives stack traces in production and uploads to error tracking without exposing maps publicly; Sentry and similar plugins upload them at build.
  • Build performance: nuxt build time is dominated by the two Vite builds and Nitro; wins come from fewer heavy dependencies, optimizeDeps for dev, avoiding giant barrel imports (tree-shaking cannot save you from import { x } from 'huge-lib' when the lib has side effects), vite.build.target matching your browsers, disabling nuxt analyze-only plugins in CI, and caching node_modules and the Nitro output between pipeline steps.
  • Migration notes: Vite 7 → 8 removes remaining Rollup-specific plugin hooks in some plugins; check plugins for Rolldown compatibility, the removed manualChunks object form, and CSS minification, which Vite 8 hands to Lightning CSS by default (build.cssMinify: 'esbuild' switches back). Nuxt manages the Vue plugin and Vite version; do not add @vitejs/plugin-vue yourself.
  • Alternatives: Nuxt supports builder: 'webpack' and 'rspack' (via @nuxt/rspack-builder) for legacy plugin ecosystems; the default and recommendation is Vite.

How it works

Tuning the client build for stable caching and a smaller entry:

nuxt.config.ts
export default defineNuxtConfig({
  sourcemap: { server: true, client: 'hidden' },
  vite: {
    build: {
      target: 'baseline-widely-available', // Vite 7+ default; explicit here
      rolldownOptions: {
        output: {
          // Rolldown: group vendor code so app changes do not invalidate it
          codeSplitting: {
            groups: [
              { name: 'vendor-vue', test: /node_modules\/(vue|@vue|vue-router|pinia)\// },
              { name: 'vendor-charts', test: /node_modules\/(echarts|d3)/ },
            ],
          },
        },
      },
    },
  },
  experimental: { entryImportMap: true }, // keeps entry chunk hash stable across route changes
})

Finding what to cut:

nuxt analyze                    # opens client and server visualisations, prints top modules
NUXT_ANALYZE=1 pnpm build       # equivalent through env in some CI setups

Build-time versus runtime configuration, side by side:

nuxt.config.ts
export default defineNuxtConfig({
  // baked at build: changing needs a rebuild
  app: { head: { title: process.env.SITE_TITLE ?? 'Shop' } },
  // read at runtime: NUXT_API_BASE / NUXT_PUBLIC_SITE_URL override per environment
  runtimeConfig: { apiBase: 'http://localhost:4000', public: { siteUrl: 'http://localhost:3000' } },
})

The output you deploy:

.output/
├── public/            static assets, /_nuxt/*.js|css with content hashes (immutable caching), prerendered HTML
├── server/
│   ├── index.mjs      Nitro entry for the preset (node-server here)
│   ├── chunks/        renderer, routes, your server/ code
│   └── node_modules/  externals Nitro decided not to inline
└── nitro.json         preset metadata
Gotcha· Secrets in the client bundle

runtimeConfig.public.* and anything referenced from client code, including via import.meta.env.VITE_*, ends up in the JavaScript that ships to browsers. Only runtimeConfig non-public keys stay on the server; grep the .output/public/_nuxt folder for a suspected value before you ship.

Gotcha· A plugin that worked on Vite 7 breaks on 8

Rolldown replaced Rollup; plugins relying on Rollup-only hooks or on this.getModuleInfo shapes may need updating. Check the plugin's Vite 8 support note before upgrading Nuxt minor versions that move the Vite major.

Verify before the interview:

Vite 8's current Node requirement, the build.minify default, the codeSplitting option shape under build.rolldownOptions.output, and the Nuxt version that made Vite 8 the default (4.5 at the time of writing).

docs ↗

Exercise

Exercise
  • Run nuxt analyze on a project and remove or lazy-load the two largest client modules; measure the entry size before and after.
  • Set sourcemap.client: 'hidden' and confirm that no .map URL is referenced from the built JavaScript while error tracking still symbolicates.
  • Move a value from import.meta.env to runtimeConfig and prove you can change it with an environment variable without rebuilding.

Be able to say

Be able to say· What happens when you run nuxt build?

"Nuxt generates its virtual app in .nuxt, then runs two Vite builds: the client build producing hashed assets in .output/public/_nuxt with a manifest, and the server build producing the SSR bundle. Nitro then bundles the server entry, the renderer and my server/ routes for the chosen preset into .output/server, inlining or externalising dependencies as configured. Since Vite 8 the bundler is Rolldown with Oxc transforms, which made builds faster. Build-time values are baked in; anything that must change per environment goes through runtimeConfig and is read from NUXT_* variables when the server starts. I check the result with nuxt analyze and ship hidden source maps to error tracking."

Your notes

Open in Notes

Nothing yet. Notes filed here show up on this page and the section overview.