Nuxt 4.5.0 was published on 18 July, and the release post calls it the biggest release in a while. The build layer moves to Vite 8, server rendering gets an experimental streaming mode, and errors now carry stable codes. Three smaller features fill gaps people have worked around for years: a useLayout() composable, named views from file names, and an enabled switch for useFetch.
Most apps upgrade with one command. If you pass options or plugins to Vite, read the Vite 8 section before you upgrade. If your pages set status codes, headers or cookies while they render, read the streaming section before you turn streaming on. The examples use the Nuxt 4 layout, with pages and components inside app/.
What shipped
The release post lists these headline changes:
- Vite 8 in the default Vite builder, and Rspack 2 for apps that use
builder: 'rspack', with that builder rebuilt on Rsbuild. - Experimental SSR streaming, which sends the start of the page before the rest has rendered.
- Stable error codes such as
NUXT_E1001, each with an explanation, a fix and, for the harder ones, a docs page. useLayout(), a read-only computed ref with the layout resolved for the current route.- Named views through a
name@view.vuefile name convention. - An
enabledoption foruseFetchanduseAsyncData.
Smaller changes in the same release:
<NuxtLink custom>now exposesprefetch,prefetchedandshouldPrefetchin its slot.import.meta.envNamegives you the environment name at runtime.- An opt-in
experimental.prefetchPreloadTagswarms the next page's preload hints when you prefetch a link to it. - Head management moves to unhead v3, which types
useHeadmore strictly (see the upgrade checks below).
The release also announces that Nuxt 3 reaches end of life on 31 July, with 3.21.9 published alongside as one of the last 3.x patches.
Vite 8 and your custom Vite config
Nuxt 4.4 built with Vite 7. Nuxt 4.5's Vite builder depends on Vite ^8.1.4, and Vite 8 changes the tools underneath:
- Rolldown, a bundler written in Rust, replaces both esbuild (for pre-bundling dependencies in development) and Rollup (for production builds).
- Oxc, a set of Rust tools for JavaScript, takes over the code transforms esbuild used to do.
The release post calls this a transparent upgrade for most apps. The apps it asks to check are the ones that depend on Vite directly: options under the vite key in nuxt.config.ts, Vite plugins you add yourself, or ecosystem plugins that pin a Vite version.
If you do pass options to Vite, read them against the Vite migration guide. Several are renamed and still work through a compatibility layer, with a deprecation:
vite.build.rollupOptionsbecomesvite.build.rolldownOptions.vite.esbuildbecomesvite.oxc.vite.optimizeDeps.esbuildOptionsbecomesvite.optimizeDeps.rolldownOptions.
One setting is not translated at all. The object form of manualChunks, often used to keep a heavy library in its own file, is no longer supported (the function form still works, but is deprecated):
export default defineNuxtConfig({
vite: {
build: {
rollupOptions: {
output: {
manualChunks: { charts: ['chart.js'] },
},
},
},
},
})
Rolldown's replacement is codeSplitting, which takes groups, each with a name and a test against the module path:
export default defineNuxtConfig({
vite: {
build: {
rolldownOptions: {
output: {
codeSplitting: {
groups: [{ name: 'charts', test: /node_modules[\\/]chart\.js[\\/]/ }],
},
},
},
},
},
})
The [\\/] parts of the test match both / and \, so the group also works with Windows paths.
Vite plugins you add yourself need a version that supports Vite 8, so check each one's changelog before you upgrade production.
Experimental SSR streaming
Without streaming, the server renders the whole page and then sends it. With experimental.ssrStreaming, Nuxt sends the HTML shell first (the <head>, styles, preload hints and entry scripts) and streams the body as Vue renders it. The aim is a lower Time to First Byte, the time until the browser receives anything at all.
export default defineNuxtConfig({
experimental: {
ssrStreaming: true,
},
routeRules: {
'/account/**': { streaming: false },
},
})
The experimental flag turns streaming on for every route, and the routeRules entry turns it off again for everything under /account/. Search engine crawlers always get the fully rendered page; a botRegex option controls what counts as a crawler.
Read the catch before you turn it on. The HTTP status and headers leave the server with the shell, so nothing can change them afterwards. According to the experimental features docs, changes made with these calls never reach the browser if they run while components render, including after an await in <script setup> or route middleware:
setResponseStatus()useResponseHeader()- a
useCookie()write - h3's
setHeader()andappendResponseHeader()
Nuxt plugins run to completion before rendering starts, so changes made there do reach the browser. Nuxt also falls back to normal rendering on its own for routes with redirect, cache, isr, swr or noScripts rules, or ssr: false. In development it logs a warning naming any dropped change, so nothing fails silently.
The practical case to check is a page that sets a 404 after loading its data. Under streaming its status is already 200 by then. Move that logic into a plugin, or opt the route out with streaming: false.
Stable error codes you can search for
Nuxt now raises its warnings and errors through nostics, the same structured-diagnostics library Pinia adopted last week. Each one has a code such as NUXT_E1001 or NUXT_B5001, a short reason and a concrete fix, and the codes are meant to stay the same between releases, so you can search for them.
The first example in the release post is the classic one: a composable such as useRoute() or useFetch() called outside a Nuxt context, usually inside a setTimeout or after an await in a plain function. That is now NUXT_E1001, and its docs page explains the rule: call the composable at the top of setup(), a plugin or middleware, and use nuxtApp.runWithContext() for async server work.
Production builds keep the code and drop the long text. The team plans to move more existing messages onto the system in coming releases.
Read the current layout with useLayout
useLayout() answers "which layout is this page using?" from anywhere. It resolves the layout the same way <NuxtLayout> does: the page's layout meta first, then a layout set through route rules, then default. It returns a read-only computed ref holding the name, or false when the layout is turned off, and it updates as the route changes:
<script setup lang="ts">
const layout = useLayout()
</script>
<template>
<SupportChat v-if="layout !== 'checkout'" />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
SupportChat stands for a component of your own, hidden here on pages that use a checkout layout. app.vue sits outside <NuxtLayout>, and useLayout() still works there: it returns the layout the current route resolves to.
Fill two outlets with named views
An outlet is a <NuxtPage> where a child page renders, and named views let one route fill two of them. Vue Router has supported them for years; Nuxt 4.5 maps them to files. A file named name@view.vue next to a page is rendered into the <NuxtPage> with that name:
docs.vue
docs/
getting-started.vue
getting-started@sidebar.vue
The parent page, docs.vue, renders both outlets and gives the second one its name:
<template>
<div>
<NuxtPage />
<aside>
<NuxtPage name="sidebar" />
</aside>
</div>
</template>
At /docs/getting-started, getting-started.vue fills the main outlet and getting-started@sidebar.vue fills the aside. An outlet with no matching file stays empty. definePageMeta is only read from the main file, so put middleware and layout settings there.
Hold back useFetch with the enabled option
Until now, stopping useFetch from running before some condition was true meant immediate: false plus a watcher that called execute(). The new enabled option takes a boolean, a ref or a getter, and works the same way in useAsyncData. While it is false, nothing runs: not the first fetch, not refresh(), not a watch trigger. Switching it from true to false cancels a request in flight and keeps the existing data.
<script setup lang="ts">
const query = ref('')
const { data, status } = await useFetch('/api/search', {
query: { q: query },
enabled: () => query.value.length > 2,
})
</script>
<template>
<input v-model="query" type="search" aria-label="Search">
<p v-if="status === 'pending'">Searching…</p>
<ul v-else-if="data">
<li v-for="hit in data" :key="hit.id">{{ hit.title }}</li>
</ul>
</template>
Typing a third character changes query, which the fetch watches, and enabled is now true, so the request goes out. One detail from the useFetch docs: turning enabled back on does not fetch by itself. A change to a watched value, or a call to refresh(), does.
How to upgrade
The release post recommends one command, which also deduplicates your lockfile so the many dependency upgrades in this release land together:
npx nuxt upgrade --dedupe
Then check three things before you deploy:
- Node. Nuxt 4.5's
enginesfield asks for Node 22.19 or later on the 22 line (4.4 accepted 22.12), 24.11 or later, or 26. - Head types. unhead v3 types
useHeadmore strictly, and promise input, deprecated in v2, is gone. The release post says new type errors are almost always genuine tightening, so fix them rather than cast them away. - Custom Vite config and plugins, as described above.
If you are still on Nuxt 3, there is a 3.x path too, npx nuxt@latest upgrade --dedupe --channel=v3, but with end of life ten days away, use this week to plan the move to Nuxt 4 instead.