Nuxt 4.5 is our biggest release in a while. Vite 8, Rspack 2 powered by Rsbuild, experimental SSR streaming, a stable error code system, a new useLayout composable, named views, and a lot of groundwork for Nuxt 5.
Nuxt 4.5 is a big one. This release ships three major upgrades to the build layer (Vite 8, Rspack 2, and a brand new Rsbuild-powered pipeline for the Rspack builder), an experimental SSR streaming mode, a stable error code system, a handful of new composables and conventions, and a lot of groundwork that brings us closer to Nuxt 5.
There's a lot here, so grab a coffee. ☕️
📣 Some News
Preparing for Nuxt 5
A good chunk of this release is (hopefully) invisible plumbing for Nuxt 5. We've moved onto the latest major versions of several core dependencies (unhead v3, unctx v3, and Vite 8), switched the framework's own build over to tsdown, and introduced a stable nuxt/* build output contract with dev exports so that type-checking in the Nuxt monorepo works without a build step (#35463, #35605).
Much of this is working to shrink the gap between v4 and v5 internally, so that the migration will be as boring(?) as possible.
If you want to test some of the breaking changes of Nuxt v4, you can already opt in with future.compatibilityVersion: 5. Keep an eye on the Upgrade Guide for details as they land.
With the release of Nuxt v4.5, our focus as a team will turn to stabilising Nuxt v5 and creating compatibility utilities to make the upgrade as smooth as possible.
Nuxt 3 End-of-Life
Nuxt 3 reaches end-of-life on July 31, 2026, so this is one of the last few 3.x releases we'll ship. If you're still on v3, now is a great time to move across. Most people told us the v3 to v4 upgrade was smooth, and we've kept the upgrade guide up to date.
Alongside v4.5.0 we're publishing a maintenance patch for the 3.x line (v3.21.9) with the compatible bug fixes and smaller improvements from this release backported. The headline items here (Vite 8, Rspack 2, unhead v3, unctx v3) are major upgrades and stay v4-only, so 3.x remains stable as it approaches end-of-life.
⚡️ Vite 8
Nuxt now runs on Vite 8 (#34256). This brings faster cold starts, the latest Rolldown-powered internals, and many upstream improvements from the Vite team.
For most apps this is a transparent upgrade. If you have custom Vite plugins or config, it's worth skimming the Vite migration guide to check for anything that affects you.
Vite 8 is a major version bump. If you depend on Vite directly (custom plugins, vite.config tweaks, or ecosystem plugins that pin a Vite version), make sure those are compatible before upgrading in production.
🦀 Rspack 2 and Rsbuild
If you use the Rspack builder, this release is a substantial upgrade. We've moved to Rspack 2 (#34929), which is faster and lighter, and rebuilt the builder on top of @rsbuild/core (#35489).
The public surface stays the same. You still opt in with builder: 'rspack' and the existing rspack:* hooks continue to work:
export default defineNuxtConfig({
builder: 'rspack',
})
Under the hood, though, a lot has changed for the better:
- The dev server now runs in middleware mode via Rsbuild, replacing
webpack-dev-middlewareandwebpack-hot-middleware(#35575). - We use an Rspack-specific Vue loader for correct SSR scoped-style ids and stricter ESM resolution (#35566).
This is the foundation for first-class Rsbuild support. We kept the builder named rspack for now so nothing breaks, but the internals are now Rsbuild all the way down.
🌊 Experimental SSR Streaming
This is one I'm particularly excited about. You can now enable SSR streaming to dramatically improve Time to First Byte (#34411). Instead of buffering the whole rendered page and sending it in one go, Nuxt flushes the HTML shell (your <head>, styles, preload hints, and entry scripts) immediately, then streams the body as Vue renders it.
export default defineNuxtConfig({
experimental: {
ssrStreaming: true,
},
})
Streaming is automatically disabled for bots and crawlers so search engines still receive fully-rendered HTML. You can tune which user agents count as crawlers, and you can opt individual routes out:
export default defineNuxtConfig({
experimental: {
ssrStreaming: {
botRegex: /googlebot|bingbot|my-internal-crawler/i,
},
},
routeRules: {
'/no-stream/**': { streaming: false },
},
})
There's one thing worth understanding before you turn it on. Because streaming commits the HTTP status and headers with the very first byte, anything that mutates the response after rendering has begun (a setResponseStatus() in a <script setup>, a cookie write during middleware, and so on) can't reach the client. Nuxt handles the common cases for you: routes with redirect, cache, isr, swr, noScripts, or ssr: false rules automatically fall back to the buffered renderer, and in development we log a warning naming any dropped mutations so nothing fails silently.
SSR streaming is experimental and off by default. It could be great for content-heavy routes where TTFB matters, but test it against your app's response-mutating logic (status codes, headers, cookies) before shipping it widely. The experimental features docs cover the fallback rules and caveats in detail.
🩺 Stable Error Codes
This one I'm really happy about. Nuxt now has a stable error code system (#35429) using nostics. Warnings and errors raised during build and at runtime now carry a stable code (like NUXT_E1001 or NUXT_B5001), a short explanation of why it happened, and a concrete fix to try.
Every code is greppable and bookmarkable, and the ones that need more than a one-line fix link straight to a dedicated docs page. For example, the classic "a composable was called outside a Nuxt context" now surfaces as NUXT_E1001 with the why/fix inline and a docs page explaining the context rules and how to use runWithContext().
To keep production output lean, the verbose why/fix text is stripped from production builds, leaving just the stable code.
This is the foundation for much better error messages across Nuxt, and we'll keep migrating existing warnings and errors onto it over the coming releases. 🔥
🎨 useLayout Composable
There's a new useLayout composable for reading the layout that's been resolved for the current route (#35623). Previously there was no clean, reactive way to ask "which layout is this page using?" from within a component.
<script setup lang="ts">
const layout = useLayout()
</script>
<template>
<span>Current layout: {{ layout }}</span>
</template>
It returns a read-only computed ref, so it stays in sync as you navigate or as route rules and definePageMeta change the resolved layout.
🪟 Named Views
Nuxt now supports named views through a filename convention (#35123). If a parent page renders more than one <NuxtPage> outlet, you can give each outlet a name and provide a sibling page file for it using the name@view.vue convention:
pages/
parent/
child.vue
child@sidebar.vue
parent.vue
<template>
<div>
<NuxtPage />
<aside>
<NuxtPage name="sidebar" />
</aside>
</div>
</template>
Navigating to /parent/child renders child.vue into the default outlet and child@sidebar.vue into the sidebar outlet. This has actually been possible in Vue Router for a long time; this release wires it up to Nuxt's file-based routing.
definePageMeta is read from the default route file only, and per-view rendering modes aren't supported (the parent page's mode applies to the default view).
🚦 enabled Option for useFetch and useAsyncData
You can now gate data fetching with a reactive enabled option (#33260). While enabled is false, every execution is blocked (the initial fetch, execute/refresh, and watch triggers), and if you flip it from true to false mid-flight, the in-flight request is cancelled without clearing your existing data.
<script setup lang="ts">
const query = ref('')
const { data } = await useFetch('/api/search', {
query: { q: query },
// Only fetch once the user has typed something
enabled: () => query.value.length > 2,
})
</script>
This is perfect for dependent or conditional queries, where you don't want to fire a request until some precondition is met. It pairs naturally with a getter or a ref, so it stays reactive.
🔗 NuxtLink Prefetch Control for Custom Slots
When you use <NuxtLink> with the custom prop, Nuxt no longer attaches prefetch handlers for you, because it can't know how you've structured your markup. To make that ergonomic, the slot now exposes everything you need to wire prefetching up yourself (#34539):
<template>
<NuxtLink
v-slot="{ href, navigate, prefetch, prefetched, shouldPrefetch }"
to="/about"
custom
>
<a
:href="href"
:class="{ 'is-prefetched': prefetched }"
@click="navigate"
@pointerenter="shouldPrefetch('interaction') && prefetch()"
@focus="shouldPrefetch('interaction') && prefetch()"
>
About page
</a>
</NuxtLink>
</template>
You get prefetch to trigger it, prefetched to know whether it's already happened (great for a prefetched class), and shouldPrefetch to respect the user's connection and config.
⚡️ Forwarded Preload Hints on Prefetch
Here's another one we'd love you to try. When you prefetch a link to a route with payload extraction, Nuxt already primes the destination's data and chunks. With the new opt-in experimental.prefetchPreloadTags (#35144), it also forwards the destination's <link rel="preload"> and modulepreload hints (whatever the page sets via useHead, or via modules like @nuxt/image's <NuxtImg preload>) into the current document, downgraded to rel="prefetch" so they don't compete with the resources the user is looking at right now.
export default defineNuxtConfig({
experimental: {
prefetchPreloadTags: true,
},
})
The practical effect is that heavy above-the-fold assets on the next page (a hero image, a critical script) start downloading while the user is still on the current one, so the navigation feels instant. It's off by default while we gather feedback, so please give it a spin and tell us how it behaves on your app.
🌐 import.meta.envName
The resolved Nuxt environment name is now available at runtime as import.meta.envName for both Vite and webpack/Rspack builds (#34844). This is the value set by --envName (or the resolved default), so you can branch on it in your app code:
if (import.meta.envName === 'staging') {
// enable staging-only behaviour
}
🔭 Tracing Channels for SSR Events
Nuxt now publishes diagnostics-channel traces for its server-side subsystems (#35191). It's unopinionated: we emit nuxt.render, nuxt.island, nuxt.data, and nuxt.plugin channels following the untracing naming convention, and you can build OpenTelemetry (or anything else) on top. It works in Node, Deno, Bun, and Cloudflare Workers.
export default defineNuxtConfig({
experimental: {
diagnosticsChannels: true,
},
})
This opens up powerful opportunities for observability, tracing, debugging, and performance monitoring, all without requiring a specific APM vendor. If you're interested in building on this, check out the diagnostics API docs.
🐛 Bug Fixes & Polish
Beyond the headline features:
- Multiple
<Suspense>boundaries now work correctly in SSR (#35360). normalizeURLnow handles relative paths correctly (#34649).- The manifest is now exposed at runtime via
useBuiltinRoutes()on the server side (#34788). - Hybrid rendering and
isPrerenderedwork together (#35427). - A new
useBreakpointscomposable helps with responsive design (#34765). - App-relative app.config imports now work with Rspack builder (#34857).
- The Vite dev middleware is now more robust to middleware registration errors (#35461).
- Automatic imports now handle multiple trailing slashes in entry points (#35558).
- Better support for rendering the same component in multiple views with
<script setup>(#35625).
🎉 Thank You
This release involved significant contributions from:
Full list of contributors visible in the repo
We're really grateful for everyone who's helped shape Nuxt 4.5. 🙌
See you in the next one! 👋
Fetched July 19, 2026
