Nuxt 3.16 shipped on 7 March 2025. It is a minor release: existing Nuxt 3 apps should keep working, and most need no code changes. Three changes are worth your attention: npm create nuxt for starting projects, delayed hydration for heavy Lazy components, and Unhead 2 under useHead(), where one kind of import should change. Startup is faster too, with nothing to configure.
If you maintain an app, start with the upgrade checklist in the last section. The examples use the Nuxt 3 layout, with pages/ and components/ at the project root.
Start a project with npm create nuxt
The release post leads with create-nuxt, a new package for starting projects. It does the same job as nuxi init, but it is about one sixth of the size, bundled as a single file with its dependencies inlined, so there is less to download before you can start. Run it from the folder that should hold the new project:
npm create nuxt
It asks where to create the project (nuxt-app unless you type another name), which package manager to use and whether to initialise a git repository. It does not ask about modules yet. The post links to the CLI roadmap, where an interactive module selector is planned; for now, add modules afterwards with npx nuxi module add <name>.
Delay hydration on Lazy components
Hydration is the step where the browser takes the HTML the server already rendered and attaches Vue to it: creating component instances, wiring event listeners, making the page interactive. Every component on the page pays that cost on load, including a footer nobody has scrolled to and a dropdown nobody has opened.
Vue 3.5 added hydration strategies that postpone this work per component. Nuxt 3.16 exposes them as props on Lazy components (an auto-imported component with the Lazy prefix, which Nuxt loads as a separate chunk), so you no longer need defineAsyncComponent by hand. This home page delays five components in five different ways:
<script setup lang="ts">
function onFooterReady() {
console.log('Footer is interactive')
}
</script>
<template>
<main>
<HeroBanner />
<LazyProductGrid hydrate-on-visible />
<LazySearchDropdown hydrate-on-interaction="mouseover" />
<LazyMobileMenu hydrate-on-media-query="(max-width: 768px)" />
<LazyNewsletterForm hydrate-on-idle />
<LazySiteFooter :hydrate-after="2000" @hydrated="onFooterReady" />
</main>
</template>
Each prop names a trigger:
hydrate-on-visiblewaits until the component scrolls into view.hydrate-on-interactionwaits for an event, here a mouseover. The event that woke the component is replayed once hydration is done.hydrate-on-media-queryhydrates only when the query matches, so the mobile menu stays inert on a wide screen.hydrate-on-idlewaits until the browser is idle.hydrate-afterwaits a number of milliseconds.
The @hydrated event fires once hydration has happened, which is useful for analytics or for starting something that needs the live component.
The HTML is still rendered on the server, so the content is visible and crawlable from the first response. Only the JavaScript work waits. Nuxt's docs for 3.16 say the props take effect only when:
- the component has the
Lazyprefix (a plain<ProductGrid />ignores them); - the prop is written in the template of a
.vuefile, not spread from an object withv-bind; - the component is auto-imported, not imported directly from
#components; - it uses one strategy, not several.
Unhead 2: check where useHead comes from
useHead() and useSeoMeta() are powered by Unhead, the library that manages the tags in <head>. Nuxt 3.16 moves to Unhead 2, which is still a release candidate on npm. The major version removes deprecations and reworks how Unhead finds its context. Nuxt 3 apps get a legacy compatibility build, so existing calls keep working, and Nuxt now re-exports the composables itself while resolving the context properly.
The check is about where your imports come from:
- The auto-imported
useHeadis fine. - An explicit import from
#app/composables/headis fine. - An import straight from
@unhead/vueis the one to change. The release post says it might lose async context: Nuxt's record of which app, and on the server which request, a call belongs to.
In 3.16 the change is recommended rather than required. During nuxi dev and nuxi build, Nuxt rewrites these imports to its own version and prints a warning that starts "You are importing from @unhead/vue in" and names the file. Search your project for from '@unhead/vue' and swap each one:
// Before: import { useHead } from '@unhead/vue'
import { useHead } from '#app/composables/head'
export function usePageTitle(title: string) {
useHead({ title: `${title} · My Shop` })
}
If you opted into future.compatibilityVersion: 4 in nuxt.config.ts, you get Unhead 2 without the compatibility build. The upgrade guide's Unhead v2 section lists what changes: the hid, vmid, children and body props are gone, promises are no longer accepted as input, tags are sorted with Capo.js, and Template Params and Alias Sorting need their plugins. unhead: { legacy: true } brings the old behaviour back while you fix them.
Faster startup and new unjs versions
The release post lists five pieces of performance work:
exsolvefor module resolution- a shorter list of resolution paths that tries direct imports first
- no duplicated Nitro alias resolution
- a
loadNuxtthat skips steps it does not need oxc-parserfor parsing Nuxt plugins
The team measured the changes with CodSpeed benchmarks in CI. The post also gives two anecdotal figures: roe.dev loads 32% faster with 3.16, and nuxt.com 28% faster. Your numbers will differ, but nuxi dev should come up sooner.
The release also moves to new major versions of the unjs packages Nuxt is built on: unenv 2 (a release candidate), ohash 2, untyped 2, c12 3, pathe 2, cookie-es 2, unimport 4 and db0 0.3, plus esbuild 0.25 and chokidar 4. Several of them now ship only ES modules. Your app does not notice, but code that loads them with require() does. If a CommonJS script of your own calls require('ohash') and gets version 2, it throws ERR_REQUIRE_ESM on Node 18 and 20. Node 22.12 and later can load an ES module that way.
Layer aliases and new config options
Four smaller additions:
- Named layer aliases. A layer is a Nuxt project that another project extends. Layers inside
~~/layers(~~is the project root) now get an alias with no config:~~/layers/shopis available as#layers/shop, so you can import from it without relative paths. A layer anywhere else can declare$meta: { name: 'shop' }in its config to get the same alias. pages.patterntells Nuxt which files count as pages. By default any.js,.ts,.vueor similar file inpages/becomes a route;pattern: ['**/*.vue']stops a stray.tsfile there from becoming one.- Granular
debug. The option takes flags (templates,modules,watchers,hooks,nitro,router,hydration) instead of the all-or-nothingdebug: true, which still works and enables everything. - Decorators, behind
experimental.decorators: true. As with every experimental feature, the team asks for feedback.
DevTools 2, clearer errors and Nitro 2.11
- Nuxt DevTools moves to version 2, with a custom editor setting, a view of the resolved config, a way to track how modules modify your config, the schema generator back, and fewer dependencies.
useAsyncDatacalls that returnundefinednow warn with an exact file location.- Island page errors show the error page correctly.
- Terminal errors go through Nitro's error handling, with clearer messages and stack traces, and Nitro applies source maps without extra Node options.
- Nitro itself moves to 2.11; its release notes list the changes.
How to upgrade and what to check first
One command refreshes the lockfile and pulls the new dependency versions, including the unjs packages:
npx nuxi@latest upgrade --dedupe
Then run through this list before you deploy:
- Import
useHead,useSeoMetaand the other head composables from Nuxt, not@unhead/vue. The build warning names any file you missed. - If you use
compatibilityVersion: 4, work through the Unhead v2 section of the upgrade guide. - If a module or script of yours loads a unjs package with
require(), check it still runs on your Node version. - Start
nuxi dev, open DevTools 2, and look at the resolved config for anything a module changed unexpectedly. - Run your tests, and compare the page source of a key page before and after: the
<head>is where an Unhead change would show.