---
title: "Nuxt 4.6"
description: "Nuxt 4.6 makes Nuxt server-agnostic with nuxt/server, rebuilds typed $fetch, adds sessions, Vue Vapor support and new dev error pages, and ships alongside Nuxt CLI v4."
canonical_url: "https://nuxt.com/blog/v4-6"
---
# Nuxt 4.6

> Nuxt 4.6 makes Nuxt server-agnostic with nuxt/server, rebuilds typed $fetch, adds sessions, Vue Vapor support and new dev error pages, and ships alongside Nuxt CLI v4.

Nuxt 4.6 is here, and it brings Nuxt CLI v4 with it. It is one of our biggest minor releases, with over 420 commits since v4.5.2, and it marks our first big step towards a server-agnostic Nuxt.

## 📣 Some News

### Nuxt CLI v4

Alongside Nuxt 4.6, today also brings a new major release of the Nuxt CLI: `@nuxt/cli` v4. It ships as a dependency of `nuxt`, so you'll get it automatically when you upgrade. Most of what's new is in `nuxt dev`.

![nuxt dev in Nuxt CLI v4](https://nuxt.com/assets/blog/v4-6/nuxt-dev.svg)

There's now an interactive panel pinned to the bottom of your terminal with URLs, startup progress and single-key shortcuts (`r` to restart, `o` to open, `l` for logs, `n` for requests, `p` for pages and server routes). Every request gets an id, so logs and errors are attributed to the request that caused them. If you prefer the classic output, pass `--no-tui`.

The dev server also tells you why it reloaded or restarted, which `nuxt.config` keys changed, where the time went during a slow start, and how long each module took to set up. A save that changes nothing no longer restarts the server, and hard restarts keep serving on the same port. Errors go through a single CLI-level channel rendered with `my-bad` ([more below](#better-errors-in-development)).

A lock file in `.nuxt/` lets a second `nuxt dev` (say, one started by an agent) take over from or defer to the one you started. The same lock file powers new `nuxt curl` and `nuxt task` commands that talk to the running server, and `nuxt docs "<query>"` searches the docs for your installed Nuxt version. You can also use `nuxt preview --takeover` to replace a running preview server.

It's also a lot smaller and starts a lot faster:

<table>
<thead>
  <tr>
    <th>
      
    </th>
    
    <th>
      v3.37
    </th>
    
    <th>
      v4.0
    </th>
    
    <th>
      
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        @nuxt/cli
      </code>
      
       install size
    </td>
    
    <td>
      13.1 MB
    </td>
    
    <td>
      3.5 MB
    </td>
    
    <td>
      <strong>
        -73%
      </strong>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        @nuxt/cli
      </code>
      
       dependencies
    </td>
    
    <td>
      70
    </td>
    
    <td>
      31
    </td>
    
    <td>
      <strong>
        -56%
      </strong>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        nuxt dev
      </code>
      
      : first paint
    </td>
    
    <td>
      330 ms
    </td>
    
    <td>
      50 ms
    </td>
    
    <td>
      <strong>
        6.6x faster
      </strong>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        nuxt dev
      </code>
      
      : port bound
    </td>
    
    <td>
      338 ms
    </td>
    
    <td>
      104 ms
    </td>
    
    <td>
      <strong>
        3.2x faster
      </strong>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        nuxt dev
      </code>
      
      : memory at rest (Linux)
    </td>
    
    <td>
      630 MB
    </td>
    
    <td>
      440 MB
    </td>
    
    <td>
      <strong>
        -30%
      </strong>
    </td>
  </tr>
</tbody>
</table>

Although this is a major version, none of the changes should be breaking for Nuxt v4 users: the CLI requires Node.js v22.21+, v24.11+ or v26+, `nuxt init` has been dropped in favour of `npm create nuxt@latest`, and Nuxt 2 and `@nuxt/bridge` are no longer supported.

::read-more{to="https://github.com/nuxt/cli/releases/tag/v4.0.0" icon="i-simple-icons-github" target="_blank"}
Read the full Nuxt CLI v4 release notes.
::

### A Server-Agnostic Nuxt

The biggest thing about this release is our move towards making Nuxt server-agnostic.

Freedom of choice is a fundamental value of the web, and one that unites the whole Nuxt team. You can use `pages/` (with vue-router) or not. You can use Vite, webpack or Rspack to bundle your code. You can pick from dozens of providers to deploy to, pick any image or font provider, choose any database adapter. In every case, the framework is the same.

The server side was different. `#app` composables imported h3 types, server code imported from `h3` and `nitropack`, and every module that touched the server was tied to whichever major version of those packages Nuxt happened to depend on. This has become particularly clear as we have been upgrading to new majors of h3 and Nitro, which ship breaking changes with a cascading effect throughout the whole ecosystem.

This release changes that. Alongside explicitly defining our public API in `@nuxt/kit` (which now does not refer to external packages), Nuxt now specifies its own types for the request event, route rules and typed `$fetch`, and exposes an import surface, [`nuxt/server`](#nuxtserver), for the server utilities most apps need. It is the culmination of work we started almost a year ago to make it possible to use any server builder with Nuxt, not just Nitro ([#33462](https://github.com/nuxt/nuxt/pull/33462)).

Under the hood, `nuxt/server` is still powered by Nitro by default. We are also announcing a second, experimental implementation, [`@nuxt/vite-server`](#experimental-nuxtvite-server), which allows pure-Vite server builds using the [Vite Environment API](https://vite.dev/guide/api-environment#environment-api).

::important
We believe that Nitro is still the right choice for almost everyone.
::

We see a number of key benefits for `nuxt/server`:

1. **It smooths the upgrade to Nuxt 5**, which moves to Nitro v3 and h3 v2. Server code written against `nuxt/server` on 4.6 runs unchanged there, so a module can ship one file for both.
2. **It decouples Nuxt from the Nitro release cycle.** Because we own the API, we can adapt to breaking changes in Nitro or h3 without requiring a new major, and we can release Nuxt majors without waiting for upstream releases.
3. **It makes Nuxt's code more maintainable.** It preserves the separation of concerns between our API (`nuxt/app` and `nuxt/server`) and the bundler and server you ultimately build your app with.

There are other benefits too, from a single type surface to being able to iterate more quickly on features.

Finally, a special thank-you to [@pi0](https://github.com/pi0), whose relentless focus on server agnosticism and work on h3, Nitro and web-standard server primitives over the last few years is what makes a portable `RequestEvent` possible at all. Thank you, Pooya. ❤️

Almost every feature in this release is already on the Nuxt 5 branch, and you can try most of the remaining Nuxt 5 defaults today with `future.compatibilityVersion: 5` ([more below](#nuxt-5-features-today)).

### Nuxt 3 End-of-Life

Nuxt 3 reached end-of-life on July 31, 2026, so there is no 3.x release alongside this one. If you're still on v3, the [upgrade guide](https://nuxt.com/docs/getting-started/upgrade) is waiting for you.

If you've read this far, I'm afraid I have bad news for you: there's a lot more still to say. You might want to grab a coffee. ☕️

## 🧩 `nuxt/server`

It has been asked for for a long time, and it now exists ([#36275](https://github.com/nuxt/nuxt/pull/36275))! 🎉

`nuxt/server` is a new import source for server code: handlers, middleware and utilities. It is the complement to `nuxt/app`, which is for the part of your application that also runs in the browser.

```ts [server/api/hello.ts]
import { defineEventHandler, getQuery } from 'nuxt/server'

export default defineEventHandler((event) => {
  const { name } = getQuery<{ name?: string }>(event)
  return { message: `Hello, ${name ?? 'world'}!` }
})
```

The utilities use web standards and are typed against a portable `RequestEvent`:

```ts
event.req         // Request
event.url         // URL
event.res         // { status, statusText, headers }
event.res.headers // Headers
event.context     // per-request context
```

Under `@nuxt/nitro-server` these are implemented with Nitro and h3, but your code doesn't import from either. So the same handler runs under Nitro v2, Nitro v3 or `@nuxt/vite-server`, and a module that imports from `nuxt/server` doesn't need a peer dependency on `h3` or `nitropack`. We think this will make a big difference in smoothing out the upgrade to Nuxt v5 and Nitro v3.

Types get simpler too. We no longer hoist h3 or Nitro types into your app to type `useRequestEvent`, `$fetch` or route rules, which removes a source of conflicts when versions differ.

The surface is small, and covers what published modules and user code typically need: `defineEventHandler`, `createError`/`isNuxtError`, request URL, headers, query, body (plain, or validated with any [Standard Schema](https://standardschema.dev) library or a function), cookies, redirects, response status, `getRouterParam(s)`, `getRequestIP`, `handleCors`, `getRouteRules`, `useRuntimeConfig`, `useAppConfig` and sessions.

```ts [server/api/users.post.ts]
import { defineEventHandler, readValidatedBody } from 'nuxt/server'
import { z } from 'zod'

export default defineEventHandler(async (event) => {
  const user = await readValidatedBody(event, z.object({ name: z.string() }))
  return { created: user.name }
})
```

We encourage you to use web APIs (`event.req.headers`, for example), or to raise an issue if there's functionality you're missing from `nuxt/server`. 🙏

If you do need to step outside `nuxt/server` for a particular handler, don't worry. Nothing has been taken away: import `defineEventHandler` and the helpers you need from `h3` or `nitropack/runtime` as before, and that handler works exactly as it did on Nuxt 4.5.

```ts [server/api/upload.post.ts]
import { defineEventHandler, readMultipartFormData } from 'h3'

export default defineEventHandler(async (event) => {
  const parts = await readMultipartFormData(event)
  return { received: parts?.length ?? 0 }
})
```

::important
On Nuxt 4, the *auto-imported* `defineEventHandler`, `getQuery` and `readBody` (and the rest of that family) are still h3's own helpers, which take a different shape of event. Import from `nuxt/server` explicitly, including `defineEventHandler`, to use the new server runtime. If you mix the two, you'll see a `NUXT_E8012` error which should tell you which import to change.
::

A few helpers also behave differently from their h3 v1 namesakes: `sendRedirect` returns the response rather than sending it, `createError` takes `status` and `statusText`, and response headers are set through `event.res.headers`. The upgrade guide has [a table of the differences](https://nuxt.com/docs/getting-started/upgrade#moving-to-nuxtserver).

Nothing in this release *requires* a migration. But if you have server code you'd like to make portable ahead of Nuxt 5, this is the best way.

::read-more{to="https://nuxt.com/docs/guide/going-further/server-imports"}
Read the server imports guide.
::

## 🔐 `appSecret` and Sessions

Thanks to [@onmax](https://github.com/onmax), Nuxt now has a root application secret: `runtimeConfig.appSecret`, set with `NUXT_APP_SECRET` ([#35874](https://github.com/nuxt/nuxt/pull/35874)). Modules and server features derive their own secrets from it with `deriveSecret(purpose)`, so it's the only secret you need to configure.

```bash
openssl rand -base64 32
```

In development, Nuxt generates and persists one if none is configured, and warns the first time a derived secret is used. Builds never generate one.

The first feature to use it is a set of session helpers in `nuxt/server` ([#36358](https://github.com/nuxt/nuxt/pull/36358)). Sessions are sealed into a cookie with [iron](https://github.com/brc-dd/iron-webcrypto), so there's no server-side storage to set up:

```ts [server/api/visits.ts]
import { defineEventHandler, useSession } from 'nuxt/server'

export default defineEventHandler(async (event) => {
  const session = await useSession<{ visits: number }>(event)
  await session.update(data => ({ visits: (data.visits ?? 0) + 1 }))
  return { visits: session.data.visits }
})
```

::note
As a reminder, runtime config keys prefixed with `app` (`runtimeConfig.app`, `runtimeConfig.appSecret`) are reserved for Nuxt.
::

## 🎯 Typed `$fetch`, Rebuilt

`$fetch` and `useFetch` have been typed from your server routes for a long time. The types came from Nitro's `InternalApi` interface, and past a few hundred routes they hit TypeScript's instantiation limit:

```text
error TS2589: Type instantiation is excessively deep and possibly infinite.
```

We've rebuilt typed fetch on top of [`fetchdts`](https://github.com/danielroe/fetchdts) ([#36238](https://github.com/nuxt/nuxt/pull/36238)). Nuxt compiles your server routes into a route tree with an exact-match table for static paths and accessors specialised to your route set. Resolution cost now scales with call sites, not with route count:

<table>
<thead>
  <tr>
    <th>
      routes
    </th>
    
    <th>
      before
    </th>
    
    <th>
      after
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      100
    </td>
    
    <td>
      1,397,361 instantiations / 0.77s
    </td>
    
    <td>
      51,558 / 0.26s
    </td>
  </tr>
  
  <tr>
    <td>
      300
    </td>
    
    <td>
      5,774,425 / 3.49s (<code>
        TS2589
      </code>
      
      )
    </td>
    
    <td>
      51,558 / 0.16s
    </td>
  </tr>
  
  <tr>
    <td>
      1000
    </td>
    
    <td>
      12,831,467 / 7.44s (<code>
        TS2589
      </code>
      
      )
    </td>
    
    <td>
      51,558 / 0.20s
    </td>
  </tr>
  
  <tr>
    <td>
      3000 (200 call sites)
    </td>
    
    <td>
      71,875,148 / 53.63s (<code>
        TS2589
      </code>
      
      )
    </td>
    
    <td>
      101,678 / 0.62s
    </td>
  </tr>
</tbody>
</table>

Peak memory for the same runs dropped from 946 MB to 140 MB. 🔥

Plus, the route set also carries the `body`, `query` and `headers` each handler validates, so calls are checked against them:

```ts
// server/api/users.post.ts validates { title: string, count: number }
await $fetch('/api/users', { method: 'post', body: { title: 'a', count: 1 } })
await $fetch('/api/users', { method: 'post', body: { title: 'a', count: 'no' } })
//                                                                ^ not assignable to number
await $fetch('/api/users', { method: 'post' })
//           ^ body is required
```

![Type error on a $fetch call with the wrong body shape](https://nuxt.com/assets/blog/v4-6/typed-fetch-error-light.png)![Type error on a $fetch call with the wrong body shape](https://nuxt.com/assets/blog/v4-6/typed-fetch-error-dark.png)

On Nuxt 4 this is opt-in, because there are small changes to type inference (a `Date` returned by a handler is a `string` at the call site, for example), and hand-written `ServerRoutes` augmentations need a small rewrite. It's the default in Nuxt 5.

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  experimental: {
    routeTypedFetch: true,
  },
})
```

There's also a new `experimental.strictRouteTypes` option to reject calls to paths that don't exist (otherwise these just return `unknown`), and an `'isomorphic'` mode that types your pages as `GET` routes too, if you want to `$fetch` them from the Vue renderer with type safety.

::read-more{to="https://nuxt.com/docs/guide/going-further/experimental-features#routetypedfetch"}
Read more about `routeTypedFetch`.
::

## 🐛 Better Errors in Development

Server-side errors in development used to look like this:

```text
ReferenceError: foo is not defined
    at Object.<anonymous> (/_nuxt/app/pages/index.vue:1:1)
```

There was no source position or code frame, and the Youch iframe we rendered could not show you the frame in your own source either. Both are now fixed ([#36258](https://github.com/nuxt/nuxt/pull/36258), [nuxt/cli#1518](https://github.com/nuxt/cli/pull/1518)).

Dev SSR stack traces are now mapped before anything reads the error, and the Youch overlay has been replaced with [`my-bad`](https://github.com/danielroe/my-bad). It renders as an overlay in your app's own error page (or as a standalone page when the app can't render one), with the mapped stack trace and a code frame from your source.

![my-bad error overlay with a mapped stack trace and code frame](https://nuxt.com/assets/blog/v4-6/my-bad-ssr-expanded-light.png)![my-bad error overlay with a mapped stack trace and code frame](https://nuxt.com/assets/blog/v4-6/my-bad-ssr-expanded-dark.png)

There's a **Copy error** button with a few formats, including a prompt you can hand straight to an agent:

![The my-bad copy menu, with Markdown, agent prompt and JSON formats](https://nuxt.com/assets/blog/v4-6/my-bad-ssr-copymenu-dark.png)

The same report is printed in your terminal:

![my-bad error report rendered in the terminal](https://nuxt.com/assets/blog/v4-6/my-bad-terminal-light.png)![my-bad error report rendered in the terminal](https://nuxt.com/assets/blog/v4-6/my-bad-terminal-dark.png)

With Nuxt CLI v4, errors also go through a single live channel at the CLI level, which survives worker restarts. A syntax error in `nuxt.config.ts` (or any other fatal startup error) is served as a page, and the page reloads once you fix it. Each error is rendered once, rather than at every layer it passes through.

![my-bad page for a module that could not be loaded from nuxt.config.ts](https://nuxt.com/assets/blog/v4-6/my-bad-fatal-light.png)![my-bad page for a module that could not be loaded from nuxt.config.ts](https://nuxt.com/assets/blog/v4-6/my-bad-fatal-dark.png)

We're working with [@atinux](https://github.com/atinux), [@HugoRCD](https://github.com/HugoRCD) and [@antfu](https://github.com/antfu) to make these pages nicer still.

## 🎨 A New Loading Screen, 404 and Error Pages

[@HugoRCD](https://github.com/HugoRCD) has redrawn the loading screen you see while the dev server starts. It's now a WebGL2 particle field that traces a mountain range behind the Nuxt lockup ([#36178](https://github.com/nuxt/nuxt/pull/36178)), using a single shader and a single draw call. Without WebGL2 it falls back to the static lockup, and with `prefers-reduced-motion` the animation stops. Try hovering over it. 🏔️

:themed-video{:autoplay="true" :controls="true" :loop="true" autoPlay="true" className="rounded-lg" dark="/assets/blog/v4-6/loading-screen-dark.mp4" dark-poster="/assets/blog/v4-6/loading-screen-dark.png" light="/assets/blog/v4-6/loading-screen-light.mp4" light-poster="/assets/blog/v4-6/loading-screen-light.png"}Hugo also gave the built-in 404 and error pages a neutral palette and lighter type ([#36255](https://github.com/nuxt/nuxt/pull/36255)), and [@MirkoJa](https://github.com/MirkoJa) added a back button to the 404 page ([#35688](https://github.com/nuxt/nuxt/pull/35688)).

![New 404 page with a back button](https://nuxt.com/assets/blog/v4-6/404-page-light.png)![New 404 page with a back button](https://nuxt.com/assets/blog/v4-6/404-page-dark.png)

![New error page](https://nuxt.com/assets/blog/v4-6/error-page-prod-light.png)![New error page](https://nuxt.com/assets/blog/v4-6/error-page-prod-dark.png)

With `experimental.prerenderErrorPages`, you can prerender error pages as real HTML (`404.html`, or any status codes you pass) instead of an empty SPA shell ([#35193](https://github.com/nuxt/nuxt/pull/35193), thanks to [@Flo0806](https://github.com/Flo0806)).

## ⚡️ Vue Vapor Support

Nuxt now supports Vue 3.6's [Vapor Mode](https://github.com/vuejs/core/releases/tag/v3.6.0-rc.1#about-vapor-mode) in interop mode ([#35759](https://github.com/nuxt/nuxt/pull/35759)). Your app root stays on the virtual DOM, and you can opt individual components or pages into Vapor with the `vapor` attribute on `<script setup>`:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  vue: {
    vapor: true,
  },
})
```

```vue [app/pages/index.vue]
<script setup vapor lang="ts">
const count = ref(0)
</script>

<template>
  <button @click="count++">
    count is {{ count }}
  </button>
</template>
```

Routing, `useAsyncData`, layouts and most built-in components work without changes. To get there, we taught the auto-import loader, `useAsyncData`, `definePageMeta` and slot inspection about Vapor, sent some fixes upstream to Vue, and added a Vapor test suite to track what's supported.

::note
This requires Vue `^3.6.0-rc.2` or newer. See the [known limitations](https://nuxt.com/docs/guide/concepts/vuejs-development#known-limitations), and try it in a fresh project first.
::

## 🔮 Nuxt 5 Features, Today

Most of what's new in Nuxt 5 is already in 4.6, either on by default or behind a flag. `future.compatibilityVersion: 5` turns on the Nuxt 5 defaults at once, and you can still toggle each one individually.

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  future: {
    compatibilityVersion: 5,
  },
})
```

This release adds these to the flag:

- typed pages (`experimental.typedPages`)
- typed `$fetch` (`experimental.routeTypedFetch`), described above
- case-sensitive routing, matching Nitro ([#35650](https://github.com/nuxt/nuxt/pull/35650), thanks to [@Mateleo](https://github.com/Mateleo))
- `experimental.navigateToEarlyReturn`: calling `navigateTo` in `<script setup>` skips the rest of setup, so redirects and 404s don't throw on missing data
- build-time extraction of serializable `definePageMeta` keys (`experimental.extractSerializablePageMeta`, [#35919](https://github.com/nuxt/nuxt/pull/35919))
- `experimental.payloadExtraction: 'client'`
- no `baseUrl` in generated tsconfigs ([#36040](https://github.com/nuxt/nuxt/pull/36040), thanks to [@oritwoen](https://github.com/oritwoen))
- inline error rendering (`experimental.inlineErrorRendering`): when a server render fails, `error.vue` is rendered in the same request with a plain try/catch instead of an internal request to `/__nuxt_error`, so headers and cookies already set are kept, the error render no longer passes through Nitro middleware and route rules a second time, and `render:html` fires with the original event ([#36399](https://github.com/nuxt/nuxt/pull/36399))
- normalized page names, `clearNuxtState` resetting to defaults, and `experimental.watcher: 'builder'`
- no auto-imported server-only head composables (`useServerHead`, `useServerHeadSafe` and `useServerSeoMeta`)

The upgrade guide lists [exactly what the flag changes on Nuxt 4](https://nuxt.com/docs/getting-started/upgrade#testing-nuxt-5).

## 🧪 Experimental: `@nuxt/vite-server`

`server.builder` has been configurable since v4.2. This release adds a second server builder: `@nuxt/vite-server`, which builds a Nuxt app with Vite alone ([#36218](https://github.com/nuxt/nuxt/pull/36218), [#36279](https://github.com/nuxt/nuxt/pull/36279), [#36288](https://github.com/nuxt/nuxt/pull/36288)).

This is all you need to configure it:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  server: {
    builder: 'vite', // 'nitro' is the default
  },
})
```

It can produce a client-only SPA, a server-rendered app with a small Node entry, a web-standard `fetch` handler for platforms that provide the server (there are e2e examples for [Cloudflare Workers](https://developers.cloudflare.com/workers/vite-plugin/), [Netlify](https://github.com/netlify/framework-adapters/tree/main/packages/vite-plugin) and [universal deploy](https://github.com/universal-deploy/universal-deploy)), as well as fully static output with `nuxt generate`.

Right now, this helps keep Nuxt's code agnostic, enforces the contract behind `nuxt/server`, and gives Vite plugins that provide a deploy target something to build on. It doesn't have Nitro's full feature set (there is no storage, caching, tasks or server plugins), and we expect most apps to keep using Nitro. **Nitro remains the default.**

::warning
This is highly experimental and the API will change. `'nitro'` and `'vite'` are new shorthands for `@nuxt/nitro-server` and `@nuxt/vite-server`.
::

## 🚀 Performance

Here's how 4.6 compares to 4.5.2 on our benchmark machine (arm64 Linux, Node.js 24.15, medians of five runs):

<table>
<thead>
  <tr>
    <th>
      
    </th>
    
    <th>
      v4.5.2
    </th>
    
    <th>
      v4.6.0
    </th>
    
    <th>
      
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        @nuxt/kit
      </code>
      
       install size
    </td>
    
    <td>
      6.4 MB
    </td>
    
    <td>
      2.1 MB
    </td>
    
    <td>
      <strong>
        -67%
      </strong>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        @nuxt/kit
      </code>
      
       transitive dependencies
    </td>
    
    <td>
      36
    </td>
    
    <td>
      22
    </td>
    
    <td>
      <strong>
        -39%
      </strong>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        nuxt build
      </code>
      
      , starter
    </td>
    
    <td>
      4.4 s
    </td>
    
    <td>
      3.6 s
    </td>
    
    <td>
      <strong>
        -18%
      </strong>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        nuxt build
      </code>
      
      , 200 pages / 200 components / 50 routes
    </td>
    
    <td>
      11.6 s
    </td>
    
    <td>
      10.3 s
    </td>
    
    <td>
      <strong>
        -11%
      </strong>
    </td>
  </tr>
  
  <tr>
    <td>
      SSR throughput, page with 300 <code>
        <NuxtLink>
      </code>
      
      s
    </td>
    
    <td>
      110 req/s
    </td>
    
    <td>
      140 req/s
    </td>
    
    <td>
      <strong>
        +26%
      </strong>
    </td>
  </tr>
</tbody>
</table>

Dev server start-up (spawn to first HTML) is about 12% faster on a starter app, and the second request takes roughly half the time. The CLI major changed at the same time, so not all of that is Nuxt. Large apps start about as fast as on 4.5.2.

The `<NuxtLink>` number comes from [#36015](https://github.com/nuxt/nuxt/pull/36015). Internal links now render on the server as a plain `<a>`, with no `useLink` and no reactive state. Rendering 200 links went from 1.36ms to 0.57ms, which is faster than a bare `<RouterLink>`.

With `experimental.early404` ([#36117](https://github.com/nuxt/nuxt/pull/36117)), page routes are compiled into a static matcher at build time. A request that can't match any page skips creating the Vue app and running plugins and middleware. On an app with 30 pages and about 23ms of boot work per render, a JSON 404 went from 37.1ms to 0.3ms.

Templates now declare what invalidates them ([#35875](https://github.com/nuxt/nuxt/pull/35875)). Before, every file change regenerated all of them. Now editing a component regenerates 0 of 49 core templates, and the `builder:watch` hook settles in 0.6ms instead of 36ms.

`useCookie` parses the cookie header once per request, or once per microtask on the client ([#36114](https://github.com/nuxt/nuxt/pull/36114)). This also means a cookie set in a plugin during SSR can be read by a later `useCookie` in a page.

Smaller changes: `ssr: false` pages are tree-shaken from the server bundle ([#35836](https://github.com/nuxt/nuxt/pull/35836), thanks to [@Austin1serb](https://github.com/Austin1serb)), `unctx` is no longer shipped to the browser and `defu` is skipped for a single `app.config` ([#36371](https://github.com/nuxt/nuxt/pull/36371)), Vite warms the server module graph before the client, crawls the client graph during warm-up, and yields to your first navigation ([#36154](https://github.com/nuxt/nuxt/pull/36154), [#35870](https://github.com/nuxt/nuxt/pull/35870), [#36412](https://github.com/nuxt/nuxt/pull/36412)), and `@nuxt/kit` has dropped `c12`, `untyped`, `confbox`, `pkg-types`, `ufo` and `mlly`, with `jiti` and `giget` now optional peers.

### Lighter Payloads

`useAsyncData` and `useFetch` accept `serialize: false` to keep data out of the payload ([#35779](https://github.com/nuxt/nuxt/pull/35779)). This is mostly useful inside components that never hydrate, and `experimental.stripNeverHydratedData` applies it automatically within `hydrate-never` component trees.

```ts
const { data } = await useFetch('/api/stats', { serialize: false })
```

In development, Nuxt now warns when a page's payload exceeds 100 kB, and when a route rendered with `noScripts` relies on client-side JavaScript. These pages also keep their non-script resource hints and styles ([#35803](https://github.com/nuxt/nuxt/pull/35803), [#36356](https://github.com/nuxt/nuxt/pull/36356), [#36359](https://github.com/nuxt/nuxt/pull/36359)). `<NuxtLink>` also prefetches server page islands, and [@atinux](https://github.com/atinux) made prefetch hints throttled and prioritised so a page full of links doesn't flood the network ([#36261](https://github.com/nuxt/nuxt/pull/36261), [#36324](https://github.com/nuxt/nuxt/pull/36324)). Building on that, route chunks, layouts, middleware, payloads, islands and resource hints now all go through one client prefetch scheduler, with per-kind concurrency caps, deduplication by key and cancellation of in-flight work when you navigate away ([#36391](https://github.com/nuxt/nuxt/pull/36391)).

## 🧩 Addons for `useFetch` and `useAsyncData`

[@cernymatej](https://github.com/cernymatej) has added an `addons` option to the `createUseFetch` and `createUseAsyncData` factories ([#35797](https://github.com/nuxt/nuxt/pull/35797)). An addon can declare custom call-site options, adjust the merged options, wrap the handler with middleware, and extend what the composable returns, and it can be reused across as many custom instances as you like.

```ts [app/composables/useApiFetch.ts]
const refreshOnFocus = defineUseFetchAddon({
  setup: (options: UseFetchAddonOptions<{ refreshOnFocus?: boolean }>) => {
    if (import.meta.server || !options.refreshOnFocus) { return }

    return (asyncData) => {
      const focused = useWindowFocus()
      watch(focused, focused => focused && asyncData.refresh())
      return { focused }
    }
  },
})

export const useApiFetch = createUseFetch({ baseURL: '/api', addons: [refreshOnFocus] })
```

```vue [app/pages/todos.vue]
<script setup lang="ts">
const { data, focused } = await useApiFetch('/todos', { refreshOnFocus: true })
</script>
```

This resolves a long list of feature requests for `useAsyncData` and `useFetch` (refresh on focus, polling, retries, auth headers and more) without making the core composables opinionated about any of them.

::read-more{to="https://nuxt.com/docs/api/utils/define-use-fetch-addon"}
Read about `defineUseFetchAddon` and `defineUseAsyncDataAddon`.
::

## 🛠️ Developer Experience

File paths in terminal output are now clickable ([#35898](https://github.com/nuxt/nuxt/pull/35898)). A warning that mentions a file links to it in your editor, at the relevant line when we know it.

![Clickable file paths in a terminal stack trace](https://nuxt.com/assets/blog/v4-6/clickable-paths-stacktrace-light.png)![Clickable file paths in a terminal stack trace](https://nuxt.com/assets/blog/v4-6/clickable-paths-stacktrace-dark.png)

Hovering a built-in component like `<NuxtLayout>` or `<NuxtLink>` in a template now shows a short description and a link to its docs ([#35701](https://github.com/nuxt/nuxt/pull/35701), thanks to [@Ibochkarev](https://github.com/Ibochkarev)).

![Hover docs for NuxtLink in the editor](https://nuxt.com/assets/blog/v4-6/ide-hover-docs-nuxt-link-light.png)![Hover docs for NuxtLink in the editor](https://nuxt.com/assets/blog/v4-6/ide-hover-docs-nuxt-link-dark.png)

Other changes:

- There's a new top-level `prerender` option, an alias for `nitro.prerender` in the same way `runtimeConfig` and `routeRules` are top-level ([#32356](https://github.com/nuxt/nuxt/pull/32356)). Nuxt now also nudges you towards top-level options where they exist, since those work across server builders ([#36416](https://github.com/nuxt/nuxt/pull/36416)).
- Linking to a file in `public/` with `<NuxtLink>` (or from Markdown with Nuxt Content) no longer needs `external`: if the router has no route for the path, Nuxt falls through to a full-page load instead of rendering your 404 ([#36169](https://github.com/nuxt/nuxt/pull/36169)).
- Renaming a component in development refreshes its imports, so you no longer see stale references to the old file ([#36165](https://github.com/nuxt/nuxt/pull/36165), thanks to [@oritwoen](https://github.com/oritwoen)).
- Layers installed from `node_modules` have their dependencies pre-bundled by Vite ([#36208](https://github.com/nuxt/nuxt/pull/36208)), and symlinked layer directories resolve to their real path ([#36402](https://github.com/nuxt/nuxt/pull/36402), thanks to [@silverbackdan](https://github.com/silverbackdan)).
- `app/types/` and `server/types/` are included in the right tsconfig, so ambient types and augmentations there are picked up ([#35783](https://github.com/nuxt/nuxt/pull/35783)). [@Flo0806](https://github.com/Flo0806) did this, and also added an augmentable `NuxtPageMeta` interface for typing `NuxtPage.meta` ([#34816](https://github.com/nuxt/nuxt/pull/34816)).
- A `(group)/` folder inside `components/` is left out of the component name ([#35699](https://github.com/nuxt/nuxt/pull/35699), thanks to [@abaza738](https://github.com/abaza738)).
- `useCookie` accepts a function for `expires` ([#35628](https://github.com/nuxt/nuxt/pull/35628), thanks to [@DarlanPrado](https://github.com/DarlanPrado)).
- Nuxt warns when a `public/` file shadows an application route ([#35674](https://github.com/nuxt/nuxt/pull/35674), thanks to [@Norbiros](https://github.com/Norbiros)).
- `typescript.tsConfig` is now the shared baseline for every generated tsconfig, with `appTsConfig` and `serverTsConfig` for per-context overrides ([#35697](https://github.com/nuxt/nuxt/pull/35697), thanks to [@chairulakmal](https://github.com/chairulakmal)).
- [@DamianGlowala](https://github.com/DamianGlowala) exported the `$Fetch` type from `nuxt/app` ([#35625](https://github.com/nuxt/nuxt/pull/35625)) and added `ShallowRef` to the Vue auto-import preset ([#36266](https://github.com/nuxt/nuxt/pull/36266)).

## 🧰 For Module Authors

If you maintain a module with server code, you can now make it compatible with both Nuxt 4 and Nuxt 5 without a major bump. (We'll also be opening PRs proactively after the release of Nuxt 4.6 to help modules prepare for Nuxt 5.)

`addServerHandler`, `addDevServerHandler` and `addNitroPlugin` accept a map of variants per server API ([#36317](https://github.com/nuxt/nuxt/pull/36317)), and Nuxt chooses the most appropriate one:

```ts [module.ts]
addServerHandler({
  route: '/api/my-module/status',
  handler: {
    nuxt: resolve('./runtime/server/status'),
    nitro2: resolve('./runtime/server/status.legacy'),
  },
})
```

A handler that imports only from `nuxt/server` doesn't need Nitro 2/3 variants at all (but does require Nuxt 4.6+). Nuxt reads the file's imports to work out which API it uses, and where that isn't enough you can declare `meta.compatibility.server`. `getNitroVersion` and `hasNitroVersion` are also there in case you have logic that *explicitly* needs to know which Nitro version is installed ([#36127](https://github.com/nuxt/nuxt/pull/36127)).

::read-more{to="https://nuxt.com/docs/guide/modules/server-compatibility"}
Read the server compatibility guide.
::

Other additions for module authors:

- augmentable server types owned by Nuxt: `ServerTypes`, `ServerRoutes`, `AppRouteRules` and `NuxtRequestContext` ([#36293](https://github.com/nuxt/nuxt/pull/36293))
- `deriveSecret`, for a module-specific secret derived from `appSecret`
- `useTerminal`, for prompts, status messages and task progress that the CLI's dev panel renders when there is one ([#36162](https://github.com/nuxt/nuxt/pull/36162))
- `module:before` and `module:done` hooks, which Nuxt CLI v4 uses to show per-module setup time ([#36173](https://github.com/nuxt/nuxt/pull/36173))
- `onConfigResolved` and `diffNuxtConfig`, to see what changed between two config loads ([#35853](https://github.com/nuxt/nuxt/pull/35853))
- `ensureDependencyInstalled` and `getAddDependencyCommand`, to offer optional dependencies using the user's package manager ([#34554](https://github.com/nuxt/nuxt/pull/34554))
- template `dependencies`, to say what should invalidate a template ([#35875](https://github.com/nuxt/nuxt/pull/35875))
- `addServerImports`, `addServerImportsDir` and `addServerTemplate` across Nitro versions, with generated server tsconfigs and versioned route config types ([#36265](https://github.com/nuxt/nuxt/pull/36265))
- an explicit public API for `@nuxt/kit` ([#36074](https://github.com/nuxt/nuxt/pull/36074)), with `@nuxt/schema` now an optional peer
- modules can set `experimental.asyncContext` ([#36175](https://github.com/nuxt/nuxt/pull/36175), thanks to [@cernymatej](https://github.com/cernymatej))
- Vite plugins added via kit are registered at the top level ([#36037](https://github.com/nuxt/nuxt/pull/36037)), and more build-time warnings use [diagnostic codes](https://nuxt.com/docs/errors) ([#36138](https://github.com/nuxt/nuxt/pull/36138))
- wider `@nuxt/kit` peer dependency ranges ([#36417](https://github.com/nuxt/nuxt/pull/36417)), and `updateRuntimeConfig` can be called before Nitro exists without a warning ([#36403](https://github.com/nuxt/nuxt/pull/36403), thanks to [@Neekoras](https://github.com/Neekoras))

## 🩹 Fixes and Security

[@oritwoen](https://github.com/oritwoen) fixed scoped styles in server component slots, duplicate island asset requests and blank remounts with lazy hydration ([#36047](https://github.com/nuxt/nuxt/pull/36047), [#36048](https://github.com/nuxt/nuxt/pull/36048), [#36051](https://github.com/nuxt/nuxt/pull/36051)). [@hdwebpros](https://github.com/hdwebpros) made `useRequestFetch` forward request headers ([#36180](https://github.com/nuxt/nuxt/pull/36180)), [@Askerka00](https://github.com/Askerka00) fixed view transitions that are skipped mid-navigation ([#35537](https://github.com/nuxt/nuxt/pull/35537)), [@Flo0806](https://github.com/Flo0806) made changed auto-import sources rescan before their consumers ([#33671](https://github.com/nuxt/nuxt/pull/33671)), and [@onmax](https://github.com/onmax) preserved error causes in development ([#35632](https://github.com/nuxt/nuxt/pull/35632)).

Other fixes cover awaited `useAsyncData` during hydration and lazy fetching ([#36124](https://github.com/nuxt/nuxt/pull/36124), [#36301](https://github.com/nuxt/nuxt/pull/36301)), `navigateTo` path encoding and `baseURL` with `open` ([#36055](https://github.com/nuxt/nuxt/pull/36055), [#36112](https://github.com/nuxt/nuxt/pull/36112), [#36197](https://github.com/nuxt/nuxt/pull/36197)), CSS extraction and inlined styles, and interrupted view transitions ([#36314](https://github.com/nuxt/nuxt/pull/36314)).

On the security side, the internal error route can only be reached by an error render, dev error reports are scoped for remote peers and kept out of production builds, and unhandled error data is no longer passed to the error page. The release notes have the details.

## ⬆️ Upgrading

Our recommendation for upgrading is to run:

```sh
npx nuxt upgrade --dedupe
```

This will refresh your lockfile and pull in the latest dependencies Nuxt relies on, including Nuxt CLI v4.

::note
This release requires Node.js `^22.22.3 || ^24.15.0 || >=26.0.0`.
::

If you have server code you'd like to make portable, read [Moving to `nuxt/server`](https://nuxt.com/docs/getting-started/upgrade#moving-to-nuxtserver) in the upgrade guide.

## 👉 Full Release Notes

::read-more{to="https://github.com/nuxt/nuxt/releases/tag/v4.6.0" icon="i-simple-icons-github" target="_blank"}
Read the full release notes of Nuxt `v4.6.0`.
::

Thank you to everyone who contributed to this release. 💚


## Sitemap

See the full [sitemap](https://nuxt.com/sitemap.md) for all pages.
