|
| 1 | +--- |
| 2 | +title: Nuxt 4.4 |
| 3 | +description: Nuxt 4.4 brings custom useFetch/useAsyncData factories, vue-router v5, a new accessibility announcer, typed layout props, build profiling, smarter payload handling, and much more. |
| 4 | +navigation: false |
| 5 | +image: /assets/blog/v4.4.png |
| 6 | +authors: |
| 7 | + - name: Daniel Roe |
| 8 | + avatar: |
| 9 | + src: https://github.com/danielroe.png |
| 10 | + to: https://bsky.app/profile/danielroe.dev |
| 11 | +date: 2026-03-12T00:00:00.000Z |
| 12 | +category: Release |
| 13 | +--- |
| 14 | + |
| 15 | +## 🏭 `createUseFetch` and `createUseAsyncData` |
| 16 | + |
| 17 | +You can now create **custom instances** of `useFetch` and `useAsyncData` with your own default options ([#32300](https://github.com/nuxt/nuxt/pull/32300)). |
| 18 | + |
| 19 | +```ts [composables/api.ts] |
| 20 | +// Simple defaults |
| 21 | +export const useClientFetch = createUseFetch({ |
| 22 | + server: false, |
| 23 | +}) |
| 24 | + |
| 25 | +// Dynamic defaults with full control over merging |
| 26 | +export const useApiFetch = createUseFetch((currentOptions) => { |
| 27 | + const runtimeConfig = useRuntimeConfig() |
| 28 | + |
| 29 | + return { |
| 30 | + ...currentOptions, |
| 31 | + baseURL: currentOptions.baseURL ?? runtimeConfig.public.baseApiUrl, |
| 32 | + } |
| 33 | +}) |
| 34 | +``` |
| 35 | + |
| 36 | +Then use them exactly like `useFetch` – they're fully typed and support all the same options: |
| 37 | + |
| 38 | +```vue [pages/dashboard.vue] |
| 39 | +<script setup lang="ts"> |
| 40 | +// Uses your baseURL from runtimeConfig automatically |
| 41 | +const { data: users } = await useApiFetch('/users') |
| 42 | +</script> |
| 43 | +``` |
| 44 | + |
| 45 | +When you pass a plain object, your usage options automatically override the defaults. When you pass a function, you get full control over how options are merged – which means you can compose interceptors, headers, and other complex options however you need. |
| 46 | + |
| 47 | +Under the hood, this is powered by a new Nuxt ad-hoc module that scans your composables directory and automatically registers your custom instances for key injection – so they work seamlessly with SSR, just like `useAsyncData` and `useFetch`. |
| 48 | + |
| 49 | +There's also `createUseAsyncData` for the same pattern with `useAsyncData`. |
| 50 | + |
| 51 | +:read-more{to="/docs/api/composables/create-use-async-data"} |
| 52 | + |
| 53 | +## 🗺️ Vue Router v5 |
| 54 | + |
| 55 | +We've upgraded to [vue-router v5](https://github.com/vuejs/router) ([#34181](https://github.com/nuxt/nuxt/pull/34181)), which removes the dependency on `unplugin-vue-router`. This is the first major vue-router upgrade since Nuxt 3, and it comes with a bunch of improvements under the hood. |
| 56 | + |
| 57 | +For most apps, this should be a transparent upgrade. If you're using `unplugin-vue-router` directly, you can remove it from your dependencies. |
| 58 | + |
| 59 | +The next step will be taking typed routes out of experimental status. 👀 |
| 60 | + |
| 61 | +## 💪 Typed Layout Props in `definePageMeta` |
| 62 | + |
| 63 | +You can now pass props to your layouts directly from `definePageMeta` ([#34262](https://github.com/nuxt/nuxt/pull/34262)). This means your layouts can be parameterised per-page without needing to use `provide`/`inject` or other workarounds. Check out the [updated docs](/docs/guide/directory-structure/layouts#layout-props) for the full details. |
| 64 | + |
| 65 | +```ts [pages/dashboard.vue] |
| 66 | +definePageMeta({ |
| 67 | + layout: { |
| 68 | + name: 'panel', |
| 69 | + props: { |
| 70 | + sidebar: true, |
| 71 | + title: 'Dashboard', |
| 72 | + }, |
| 73 | + }, |
| 74 | +}) |
| 75 | +``` |
| 76 | + |
| 77 | +Even better – the props are **fully typed** ([#34409](https://github.com/nuxt/nuxt/pull/34409)). If your layout defines props, you'll get autocomplete and type-checking in `definePageMeta`. |
| 78 | + |
| 79 | +```vue [layouts/panel.vue] |
| 80 | +<script setup lang="ts"> |
| 81 | +defineProps<{ |
| 82 | + sidebar?: boolean |
| 83 | + title?: string |
| 84 | +}>() |
| 85 | +</script> |
| 86 | +``` |
| 87 | + |
| 88 | +:read-more{to="/docs/guide/directory-structure/layouts"} |
| 89 | + |
| 90 | +## 🗣️ `useAnnouncer` Composable |
| 91 | + |
| 92 | +Accessibility got a major boost with the new `useAnnouncer` composable and `<NuxtAnnouncer>` component ([#34318](https://github.com/nuxt/nuxt/pull/34318)). While `useRouteAnnouncer` handles page navigation for screen readers, many apps need to announce **dynamic in-page changes** – form submissions, loading states, search results, and more. |
| 93 | + |
| 94 | +::code-group |
| 95 | +```vue [app.vue] |
| 96 | +<template> |
| 97 | + <NuxtAnnouncer /> |
| 98 | + <NuxtRouteAnnouncer /> |
| 99 | + <NuxtLayout> |
| 100 | + <NuxtPage /> |
| 101 | + </NuxtLayout> |
| 102 | +</template> |
| 103 | +``` |
| 104 | + |
| 105 | +```vue [components/ContactForm.vue] |
| 106 | +<script setup lang="ts"> |
| 107 | +const { polite, assertive } = useAnnouncer() |
| 108 | +
|
| 109 | +async function submitForm() { |
| 110 | + try { |
| 111 | + await $fetch('/api/contact', { method: 'POST', body: formData }) |
| 112 | + polite('Message sent successfully') |
| 113 | + } |
| 114 | + catch (error) { |
| 115 | + assertive('Error: Failed to send message') |
| 116 | + } |
| 117 | +} |
| 118 | +</script> |
| 119 | +``` |
| 120 | +:: |
| 121 | + |
| 122 | +::note |
| 123 | +This is part of our [accessibility roadmap](https://github.com/nuxt/nuxt/issues/23255). You don't need to use it everywhere – for many interactions, moving focus to new content or using native form validation is sufficient. `useAnnouncer` is most useful when content changes dynamically without a corresponding focus change. |
| 124 | +:: |
| 125 | + |
| 126 | +:read-more{to="/docs/api/composables/use-announcer"} |
| 127 | + |
| 128 | +## 🚀 Migrate to `unrouting` |
| 129 | + |
| 130 | +We've migrated Nuxt's file-system route generation to [`unrouting`](https://github.com/unjs/unrouting) ([#34316](https://github.com/nuxt/nuxt/pull/34316)), which uses a trie data structure for constructing routes. The cold start is roughly the same (~8ms vs ~6ms for large apps), but dev server changes are **up to 28x faster** when you're not adding/removing pages, and ~15% faster even when you are. |
| 131 | + |
| 132 | +This also makes route generation more deterministic – it's no longer sensitive to page file ordering. |
| 133 | + |
| 134 | +## 🍫 Smarter Payload Handling for Cached Routes |
| 135 | + |
| 136 | +When a cached route (ISR/SWR) is rendered at runtime with payload extraction enabled, the browser immediately fetches `_payload.json` as a second request – which triggers a full SSR re-render of the same page. In serverless environments, this can spin up a second lambda before the first response has even finished streaming. |
| 137 | + |
| 138 | +This release addresses this with two changes ([#34410](https://github.com/nuxt/nuxt/pull/34410)): |
| 139 | + |
| 140 | +1. A new `payloadExtraction: 'client'` mode that inlines the full payload in the initial HTML response while still generating `_payload.json` for client-side navigation |
| 141 | +2. A runtime in-memory LRU payload cache so that `_payload.json` requests can be served without a full re-render |
| 142 | + |
| 143 | +```ts [nuxt.config.ts] |
| 144 | +export default defineNuxtConfig({ |
| 145 | + experimental: { |
| 146 | + payloadExtraction: 'client', |
| 147 | + }, |
| 148 | +}) |
| 149 | +``` |
| 150 | + |
| 151 | +::note |
| 152 | +`payloadExtraction: 'client'` will become the default with `compatibilityVersion: 5`. The runtime cache is active for all users. |
| 153 | +:: |
| 154 | + |
| 155 | +:read-more{to="/docs/guide/going-further/experimental-features#payloadextraction"} |
| 156 | + |
| 157 | +## 🍪 `refresh` Option for `useCookie` |
| 158 | + |
| 159 | +If you're using cookies for session management, you've probably run into the problem of needing to extend a cookie's expiration without changing its value. The new `refresh` option makes this simple ([#33814](https://github.com/nuxt/nuxt/pull/33814)): |
| 160 | + |
| 161 | +```ts |
| 162 | +const session = useCookie('session-id', { |
| 163 | + maxAge: 60 * 60, |
| 164 | + refresh: true, |
| 165 | +}) |
| 166 | + |
| 167 | +// Extends expiration each time, even with the same value |
| 168 | +session.value = session.value |
| 169 | +``` |
| 170 | + |
| 171 | +:read-more{to="/docs/api/composables/use-cookie"} |
| 172 | + |
| 173 | +## ♻️ `useState` Reset to Default |
| 174 | + |
| 175 | +`useState` and `clearNuxtState` now support resetting to the initial value instead of clearing to `undefined` ([#33527](https://github.com/nuxt/nuxt/pull/33527)). This aligns with how `useAsyncData` handles resets and is more intuitive for state management. |
| 176 | + |
| 177 | +```ts |
| 178 | +const count = useState('counter', () => 0) |
| 179 | +count.value = 42 |
| 180 | + |
| 181 | +// Resets to 0 (the init value), not undefined |
| 182 | +clearNuxtState('counter') |
| 183 | +``` |
| 184 | + |
| 185 | +:read-more{to="/docs/api/utils/clear-nuxt-state"} |
| 186 | + |
| 187 | +## 🕵️♂️ Better Import Protection |
| 188 | + |
| 189 | +Inspired by features in [TanStack Start](https://tanstack.com/start/latest/docs/framework/react/guide/import-protection), import protection now shows **suggestions** and a full **trace** of where a problematic import originated ([#34454](https://github.com/nuxt/nuxt/pull/34454)). This makes it much easier to debug why a server-only import ended up in your client bundle. |
| 190 | + |
| 191 | +For example, if you accidentally import from a server route in a component: |
| 192 | + |
| 193 | + |
| 194 | + |
| 195 | +The trace shows the import chain (the component was imported from a page), the exact line of code, and actionable suggestions for how to fix it. |
| 196 | + |
| 197 | +We plan to continue work on improving our error messages. 🪵 |
| 198 | + |
| 199 | +## 🔮 View Transitions Types |
| 200 | + |
| 201 | +You can now define [view transition types](https://developer.chrome.com/blog/view-transitions-update-io24#view-transition-types) in Nuxt's experimental view transitions support ([#31982](https://github.com/nuxt/nuxt/pull/31982)). This lets you use different transition styles for different navigation patterns (forwards vs. backwards, tabs vs. pages, etc.). |
| 202 | + |
| 203 | +:read-more{to="/docs/getting-started/transitions#view-transitions-api-experimental"} |
| 204 | + |
| 205 | +## 💡 Improved `optimizeDeps` Hints |
| 206 | + |
| 207 | +When Vite discovers new dependencies at runtime and triggers page reloads, Nuxt now shows a clear, copy-pasteable `nuxt.config.ts` snippet to pre-bundle them ([#34320](https://github.com/nuxt/nuxt/pull/34320)). It also warns about unresolvable entries at startup. |
| 208 | + |
| 209 | +## 🏷️ Normalised Page Component Names (Experimental) |
| 210 | + |
| 211 | +A new experimental option normalises page component names to match route names ([#33513](https://github.com/nuxt/nuxt/pull/33513)), which can help with consistency in devtools and debugging. |
| 212 | + |
| 213 | +```ts [nuxt.config.ts] |
| 214 | +export default defineNuxtConfig({ |
| 215 | + experimental: { |
| 216 | + normalizeComponentNames: true, |
| 217 | + }, |
| 218 | +}) |
| 219 | +``` |
| 220 | + |
| 221 | +:read-more{to="/docs/guide/going-further/experimental-features#normalizecomponentnames"} |
| 222 | + |
| 223 | +## ⚡ Build Profiling |
| 224 | + |
| 225 | +Ever wondered where your build time goes? You can now get a detailed performance breakdown of your Nuxt builds ([#34468](https://github.com/nuxt/nuxt/pull/34468), [nuxt/cli#1243](https://github.com/nuxt/cli/pull/1243)): |
| 226 | + |
| 227 | +```bash |
| 228 | +nuxt build --profile |
| 229 | +``` |
| 230 | + |
| 231 | +This produces a report showing duration, RSS delta, and heap delta for every build phase, module, and bundler plugin: |
| 232 | + |
| 233 | + |
| 234 | + |
| 235 | +It also profiles individual modules and bundler plugins, making it easy to spot bottlenecks. Three output formats are written: |
| 236 | + |
| 237 | +- **Chrome Trace** (`.nuxt/perf-trace.json`) – open in `chrome://tracing` or Perfetto for a visual timeline |
| 238 | +- **JSON report** (`.nuxt/perf-report.json`) – machine-readable data for tracking over time |
| 239 | +- **CPU profile** (`nuxt-build.cpuprofile`) – open in Chrome DevTools or VS Code for flame graphs |
| 240 | + |
| 241 | +For even more detail, use `--profile=verbose` to print timing breakdowns to the console. |
| 242 | + |
| 243 | +:read-more{to="/docs/api/commands/build"} |
| 244 | + |
| 245 | +We'll be using this feature to make Nuxt even faster – and if performance is something you care about, this might be a good opportunity to contribute! |
| 246 | + |
| 247 | +## 🔥 Performance Improvements |
| 248 | + |
| 249 | +- **14,000x faster module ID parsing** – replaced `new URL()` + regex chain with a single `indexOf` + slice ([#34451](https://github.com/nuxt/nuxt/pull/34451)) |
| 250 | +- **Disabled NuxtLink visibility prefetching in dev** – stops Vite from discovering and reloading deps unnecessarily during development ([#34325](https://github.com/nuxt/nuxt/pull/34325)) |
| 251 | + |
| 252 | +## ⬆︎ Upgrading |
| 253 | + |
| 254 | +Our recommendation for upgrading is to run: |
| 255 | + |
| 256 | +```sh |
| 257 | +npx nuxt upgrade --dedupe |
| 258 | +``` |
| 259 | + |
| 260 | +This will deduplicate your lockfile and help ensure you pull in updates from other dependencies that Nuxt relies on, particularly in the unjs ecosystem. |
| 261 | + |
| 262 | +::tip |
| 263 | +Check out our [upgrade guide](/docs/getting-started/upgrade) if upgrading from an older version. |
| 264 | +:: |
| 265 | + |
| 266 | +## 👉 Full Release Notes |
| 267 | + |
| 268 | +::read-more |
| 269 | +--- |
| 270 | +icon: i-simple-icons-github |
| 271 | +target: _blank |
| 272 | +to: https://github.com/nuxt/nuxt/releases/tag/v4.4.0 |
| 273 | +--- |
| 274 | +Read the full release notes of Nuxt `v4.4.0`. |
| 275 | +:: |
| 276 | + |
| 277 | +Thank you to all of the many contributors to this release! 💚 |
0 commit comments