Руководство по обновлению

Как обновиться до последней версии Nuxt.

Обновление Nuxt

Последний релиз

Чтобы обновиться до последнего релиза, выполните команду nuxt upgrade.

npx nuxt upgrade

Канал Nightly

Чтобы использовать свежие сборки Nuxt и тестировать функции до релиза, см. канал nightly.

Тестирование Nuxt 5

Nuxt 5 сейчас в разработке. До релиза многие breaking changes можно попробовать уже в Nuxt 4.2+.

Включение режима Nuxt 5

Сначала обновите Nuxt до последнего релиза.

Затем задайте future.compatibilityVersion: 5 для поведения Nuxt 5:

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

При future.compatibilityVersion: 5 значения по умолчанию в конфигурации Nuxt переключаются на поведение Nuxt v5:

  • Vite Environment API: автоматически включается новый Vite Environment API для улучшенной конфигурации сборки
  • Остальные изменения Nuxt 5 по мере появления
Раздел может меняться до финального релиза; при тестировании Nuxt 5 с future.compatibilityVersion: 5 заглядывайте сюда регулярно.

Критичные изменения перечислены ниже вместе с шагами миграции для обратной совместимости.

Миграция на Vite Environment API

🚦 Уровень влияния: средний

Что изменилось

Nuxt 5 переходит на новый Environment API Vite 6: формализовано понятие окружений и улучшен контроль конфигурации для каждого.

Раньше Nuxt использовал отдельные конфиги Vite для клиента и сервера. Теперь — общий конфиг Vite с плагинами под окружения через метод applyToEnvironment().

Проверить возможность заранее: задать future.compatibilityVersion: 5 (см. Тестирование Nuxt 5) или явно включить experimental.viteEnvironmentApi: true.

Основные изменения:

  1. Устарели опции окружений в extendViteConfig(): опции server и client в extendViteConfig() устарели, при использовании выводятся предупреждения.
  2. Регистрация плагинов: у плагинов Vite, зарегистрированных через addVitePlugin() и нацеленных только на одно окружение (server: false или client: false), хуки config и configResolved не вызываются.
  3. Общая конфигурация: хуки vite:extendConfig и vite:configResolved работают с общей конфигурацией вместо отдельных клиентской и серверной.

Причины изменений

Vite Environment API даёт:

  • Более предсказуемое поведение в dev и production
  • Точный контроль конфигурации по окружениям
  • Улучшенную производительность и архитектуру плагинов
  • Поддержку произвольных окружений, не только client/server

Шаги миграции

1. Переход на плагины Vite

Рекомендуется использовать плагин Vite вместо extendViteConfig, vite:configResolved и vite:extendConfig.

// Было
extendViteConfig((config) => {
  config.optimizeDeps.include.push('my-package')
}, { server: false })

nuxt.hook('vite:extendConfig' /* or vite:configResolved */, (config, { isClient }) => {
  if (isClient) {
    config.optimizeDeps.include.push('my-package')
  }
})

// Стало
addVitePlugin(() => ({
  name: 'my-plugin',
  config (config) {
    // здесь можно задать глобальную конфигурацию Vite
  },
  configResolved (config) {
    // здесь доступна полностью разрешённая конфигурация Vite
  },
  configEnvironment (name, config) {
    // здесь — конфигурация для конкретного окружения
    if (name === 'client') {
      config.optimizeDeps ||= {}
      config.optimizeDeps.include ||= []
      config.optimizeDeps.include.push('my-package')
    }
  },
  applyToEnvironment (environment) {
    return environment.name === 'client'
  },
}))
2. Переход плагинов Vite на окружения

Вместо addVitePlugin с server: false или client: false используйте в плагине хук applyToEnvironment.

// Было
addVitePlugin(() => ({
  name: 'my-plugin',
  config (config) {
    config.optimizeDeps.include.push('my-package')
  },
}), { client: false })

// Стало
addVitePlugin(() => ({
  name: 'my-plugin',
  config (config) {
    // здесь можно задать глобальную конфигурацию Vite
  },
  configResolved (config) {
    // здесь доступна полностью разрешённая конфигурация Vite
  },
  configEnvironment (name, config) {
    // здесь — конфигурация для конкретного окружения
    if (name === 'client') {
      config.optimizeDeps ||= {}
      config.optimizeDeps.include ||= []
      config.optimizeDeps.include.push('my-package')
    }
  },
  applyToEnvironment (environment) {
    return environment.name === 'client'
  },
}))
Подробнее об Environment API Vite

Миграция на Nuxt 4

Nuxt 4 вносит существенные изменения. Это руководство поможет перенести приложение с Nuxt 3 на Nuxt 4.

Сначала обновитесь до Nuxt 4:

npm install nuxt@^4.0.0

После обновления поведение Nuxt 4 по умолчанию включено. Часть возможностей можно настроить для обратной совместимости во время миграции.

Ниже — основные изменения и шаги миграции при переходе на Nuxt 4.

Критичные изменения описаны вместе с шагами миграции и доступными опциями конфигурации.

Миграция с помощью Codemods

Вместе с командой Codemod подготовлены открытые codemods для автоматизации многих шагов миграции.

О проблемах с codemods сообщайте команде Codemod: npx codemod feedback 🙏

Полный список codemods для Nuxt 4, описание и способы запуска — в Codemod Registry.

Все codemods из этого руководства можно запустить одним рецептом:

# Using pinned version due to https://github.com/codemod/codemod/issues/1710
npx codemod@0.18.7 nuxt/4/migration-recipe

Команда выполнит все codemods по очереди; ненужные можно отключить. Каждый codemod описан ниже рядом с соответствующим изменением и может быть запущен отдельно.

Новая структура каталогов

🚦 Уровень влияния: значительный

В Nuxt 4 по умолчанию используется новая структура каталогов; при этом сохранена обратная совместимость (если Nuxt видит старую структуру, например app/pages/ в корне, новая не применяется).

👉 Полный RFC

Что изменилось

  • По умолчанию srcDirapp/, большинство путей разрешается относительно неё.
  • serverDir по умолчанию <rootDir>/server, а не <srcDir>/server.
  • layers/, modules/ и public/ по умолчанию разрешаются относительно <rootDir>.
  • При использовании Nuxt Content v2.13+ каталог content/ разрешается относительно <rootDir>.
  • Добавлен dir.app — каталог, в котором ищутся router.options.ts и spa-loading-template.html (по умолчанию <srcDir>/).
Пример структуры каталогов в v4.
.output/
.nuxt/
app/
  assets/
  components/
  composables/
  layouts/
  middleware/
  pages/
  plugins/
  utils/
  app.config.ts
  app.vue
  router.options.ts
content/
layers/
modules/
node_modules/
public/
shared/
server/
  api/
  middleware/
  plugins/
  routes/
  utils/
nuxt.config.ts
При новой структуре алиас ~ по умолчанию указывает на каталог app/ (ваш srcDir). То есть ~/componentsapp/components/, ~/pagesapp/pages/ и т.д.

👉 Подробнее: PR с реализацией.

Причины изменений

  1. Производительность — размещение всего кода в корне репозитория приводит к сканированию .git/ и node_modules/ файловыми watchers и замедлению запуска (особенно не на macOS).
  2. Типизация в IDEserver/ и остальное приложение работают в разных контекстах с разными глобальными импортами; вынос server/ из каталога приложения улучшает автодополнение в IDE.

Шаги миграции

  1. Создайте каталог app/.
  2. Перенесите в него assets/, components/, composables/, layouts/, middleware/, pages/, plugins/, utils/, а также app.vue, error.vue, app.config.ts. Пути к router.options.ts и spa-loading-template.html при необходимости остаются в app/.
  3. Оставьте в корне проекта (вне app/): nuxt.config.ts, content/, layers/, modules/, public/, server/.
  4. Обновите конфиги сторонних инструментов (tailwindcss, eslint и т.д.) под новую структуру. @nuxtjs/tailwindcss обычно настраивает tailwind автоматически.
Миграцию можно автоматизировать: npx codemod@latest nuxt/4/file-structure

Миграция не обязательна: при текущей структуре Nuxt должен определить её сам (если нет — создайте issue). Исключение — кастомный srcDir: тогда modules/, public/ и server/ разрешаются от rootDir, а не от srcDir. Переопределить можно через dir.modules, dir.public и serverDir.

Вернуть структуру каталогов как в v3 можно такой конфигурацией:

nuxt.config.ts
export default defineNuxtConfig({
  // Возврат srcDir в корень проекта
  srcDir: '.',
  // Каталог для router.options.ts и spa-loading-template.html
  dir: {
    app: 'app',
  },
})

Единый слой загрузки данных

🚦 Уровень влияния: средний

Что изменилось

Система загрузки данных Nuxt (useAsyncData и useFetch) переработана для производительности и единообразия:

  1. Общие ref при одном ключе: все вызовы useAsyncData/useFetch с одним ключом используют одни и те же refs data, error и status. Важно, чтобы при явном ключе не было конфликтующих опций deep, transform, pick, getCachedData или default.
  2. Контроль getCachedData: функция getCachedData теперь вызывается при каждой загрузке, в том числе при срабатывании watcher или вызове refreshNuxtData. (Раньше в этих случаях данные всегда запрашивались заново, а функция не вызывалась.) Для гибкого выбора «кэш или повторный запрос» функция получает контекст с причиной запроса.
  3. Реактивные ключи: в качестве ключа можно передавать computed ref, обычный ref или getter — данные будут перезапрашиваться автоматически и храниться раздельно.
  4. Очистка данных: когда размонтируется последний компонент, использующий данные по ключу, Nuxt удаляет эти данные, чтобы не раздувать память.

Причины изменений

Цель — снизить потребление памяти и унифицировать состояния загрузки между вызовами useAsyncData.

Шаги миграции

  1. Проверьте согласованность опций: найдите компоненты, где один ключ используется с разными опциями или разными функциями запроса.
    // This will now trigger a warning
    const { data: users1 } = useAsyncData('users', () => $fetch('/api/users'), { deep: false })
    const { data: users2 } = useAsyncData('users', () => $fetch('/api/users'), { deep: true })
    

    Вынесите вызовы useAsyncData с общим ключом и кастомными опциями в отдельный композабл:
    app/composables/useUserData.ts
    export function useUserData (userId: string) {
      return useAsyncData(
        `user-${userId}`,
        () => fetchUser(userId),
        {
          deep: true,
          transform: user => ({ ...user, lastAccessed: new Date() }),
        },
      )
    }
    
  2. Обновите реализации getCachedData:
    useAsyncData('key', fetchFunction, {
    -  getCachedData: (key, nuxtApp) => {
    -    return cachedData[key]
    -  }
    +  getCachedData: (key, nuxtApp, ctx) => {
    +    // ctx.cause — 'initial' | 'refresh:hook' | 'refresh:manual' | 'watch'
    +    
    +    // Пример: не использовать кэш при ручном обновлении
    +    if (ctx.cause === 'refresh:manual') return undefined
    +    
    +    return cachedData[key]
    +  }
    })
    

Временно отключить это поведение можно так:

nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    granularCachedData: false,
    purgeCachedData: false,
  },
})

Исправленный порядок загрузки модулей в слоях

🚦 Уровень влияния: минимальный

Что изменилось

Исправлен порядок загрузки модулей при использовании слоёв Nuxt. Раньше модули из корня проекта загружались раньше модулей из расширяемых слоёв — наоборот ожидаемому.

Теперь порядок такой:

  1. Сначала модули слоёв (в порядке extends — более глубокие слои первыми)
  2. Затем модули проекта (наивысший приоритет)

Это касается и модулей из массива modules в nuxt.config.ts, и автообнаруживаемых в каталоге modules/.

Причины изменений

Так обеспечиваются:

  • меньший приоритет слоёв по сравнению с проектом
  • интуитивный порядок выполнения при наследовании слоёв
  • корректная работа конфигурации и хуков в многослойной настройке

Шаги миграции

В большинстве проектов менять ничего не нужно — порядок приведён к ожидаемому.

Если вы полагались на старый порядок, возможно потребуется:

  1. Проверить зависимости модулей: есть ли модули, зависящие от порядка загрузки.
  2. Скорректировать конфигурацию: если она обходила старый порядок.
  3. Прогнать тесты: убедиться, что всё работает при новом порядке.

Пример нового порядка:

// Layer: my-layer/nuxt.config.ts
export default defineNuxtConfig({
  modules: ['layer-module-1', 'layer-module-2'],
})

// Project: nuxt.config.ts
export default defineNuxtConfig({
  extends: ['./my-layer'],
  modules: ['project-module-1', 'project-module-2'],
})

// Порядок загрузки (исправленный):
// 1. layer-module-1
// 2. layer-module-2
// 3. project-module-1 (может переопределять модули слоя)
// 4. project-module-2 (может переопределять модули слоя)

Если порядок модулей мешает регистрации хука, используйте хук modules:done — он выполняется после загрузки всех модулей.

👉 Подробнее: PR #31507, issue #25719.

Дедупликация метаданных маршрута

🚦 Уровень влияния: минимальный

Что изменилось

Часть метаданных маршрута задаётся через definePageMeta (name, path и т.д.). Раньше они были доступны и в объекте маршрута, и в route.meta (например route.name и route.meta.name).

Теперь доступ только через объект маршрута.

Причины изменений

Следствие включения experimental.scanPageMeta по умолчанию и оптимизации производительности.

Шаги миграции

Обычно достаточно заменить обращение:

  const route = useRoute()
  
- console.log(route.meta.name)
+ console.log(route.name)

Нормализованные имена компонентов

🚦 Уровень влияния: средний

Vue теперь генерирует имена компонентов по правилам Nuxt.

Что изменилось

По умолчанию (если имя не задано вручную) Vue присваивает компоненту имя по имени файла.

Структура каталогов
├─ components/
├─── SomeFolder/
├───── MyComponent.vue

Для Vue имя компонента — MyComponent (для <KeepAlive> и Vue DevTools). Для автоимпорта раньше нужно было использовать SomeFolderMyComponent.

Теперь оба варианта совпадают: Vue выдаёт имя по правилам Nuxt.

Шаги миграции

В тестах с findComponent из @vue/test-utils и в <KeepAlive> по имени компонента используйте обновлённое имя.

Временно отключить это поведение можно так:

nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    normalizeComponentNames: false,
  },
})

Unhead v2

🚦 Уровень влияния: минимальный

Что изменилось

Unhead, используемый для тегов <head>, обновлён до v2. В целом совместим, но есть breaking changes в низкоуровневом API.

  • Удалены пропсы: vmid, hid, children, body.
  • Ввод в виде Promise больше не поддерживается.
  • Теги по умолчанию сортируются через Capo.js.

Шаги миграции

На большинство приложений это почти не повлияет.

При проблемах проверьте:

  • Не используются ли удалённые пропсы.
useHead({
  meta: [{ 
    name: 'description', 
    // meta tags don't need a vmid, or a key    
-   vmid: 'description' 
-   hid: 'description'
  }]
})
import { AliasSortingPlugin, TemplateParamsPlugin } from '@unhead/vue/plugins'

export default defineNuxtPlugin({
  setup () {
    const unhead = injectHead()
    unhead.use(TemplateParamsPlugin)
    unhead.use(AliasSortingPlugin)
  },
})

Не обязательно, но рекомендуется заменить импорты из @unhead/vue на #imports или nuxt/app.

-import { useHead } from '@unhead/vue'
+import { useHead } from '#imports'

При проблемах можно вернуть поведение v1, включив опцию head.legacy.

export default defineNuxtConfig({
  unhead: {
    legacy: true,
  },
})

Новое расположение экрана загрузки SPA в DOM

🚦 Уровень влияния: минимальный

Что изменилось

При рендере клиентской страницы (ssr: false) экран загрузки (из ~/app/spa-loading-template.html; в Nuxt 4 путь — ~/spa-loading-template.html) раньше рендерился внутри корня приложения Nuxt:

<div id="__nuxt">
  <!-- шаблон загрузки SPA -->
</div>

Теперь по умолчанию шаблон рендерится рядом с корнем:

<div id="__nuxt"></div>
<!-- шаблон загрузки SPA -->

Причины изменений

Шаблон загрузки остаётся в DOM до разрешения suspense приложения Vue, что убирает белую вспышку.

Шаги миграции

Если вы обращались к шаблону загрузки через CSS или document.querySelector, обновите селекторы. Можно использовать новые опции app.spaLoaderTag и app.spaLoaderAttrs.

Вернуть прежнее поведение можно так:

nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    spaLoadingTemplateLocation: 'within',
  },
})

Парсинг error.data

🚦 Уровень влияния: минимальный

Раньше при выбросе ошибки с полем data оно не парсилось. Теперь оно парсируется и доступно в объекте error. Это исправление, но breaking change для кода, который парсил error.data вручную.

Шаги миграции

В кастомном error.vue уберите ручной парсинг error.data:

  <script setup lang="ts">
  import type { NuxtError } from '#app'

  const props = defineProps({
    error: Object as () => NuxtError
  })

- const data = JSON.parse(error.data)
+ const data = error.data
  </script>

Более точечная инлайнизация стилей

🚦 Уровень влияния: средний

Nuxt теперь инлайнит только стили Vue-компонентов, не глобальный CSS.

Что изменилось

Раньше Nuxt инлайнил весь CSS, включая глобальный, и убирал <link> на отдельные файлы. Теперь инлайнятся только стили компонентов (ранее из них получались отдельные чанки). Это даёт и меньше отдельных запросов при первой загрузке, и кэширование одного глобального CSS, и меньший размер документа.

Шаги миграции

Поведение настраивается: вернуть инлайн глобального CSS можно опцией inlineStyles: true.

nuxt.config.ts
export default defineNuxtConfig({
  features: {
    inlineStyles: true,
  },
})

Сканирование метаданных страниц после разрешения

🚦 Уровень влияния: минимальный

Что изменилось

Метаданные страниц (из definePageMeta) теперь сканируются после хука pages:extend, а не до него.

Причины изменений

Так учитываются страницы, добавленные в pages:extend. Переопределять метаданные можно в новом хуке pages:resolved.

Шаги миграции

Переопределение метаданных страниц перенесите из pages:extend в pages:resolved.

  export default defineNuxtConfig({
    hooks: {
-     'pages:extend'(pages) {
+     'pages:resolved'(pages) {
        const myPage = pages.find(page => page.path === '/')
        myPage.meta ||= {}
        myPage.meta.layout = 'overridden-layout'
      }
    }
  })

Вернуть прежнее поведение можно так:

nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    scanPageMeta: true,
  },
})

Общие данные при пререндере

🚦 Уровень влияния: средний

Что изменилось

Включена ранее экспериментальная возможность делиться данными из useAsyncData и useFetch между страницами. См. исходный PR.

Причины изменений

Данные payload автоматически переиспользуются между пререндеренными страницами. Это ускоряет пререндер сайтов, где на разных страницах запрашиваются одни и те же данные.

Например, если на каждой странице вызывается useFetch (меню, настройки из CMS), при пререндере данные запросятся один раз и закэшируются для остальных страниц.

Шаги миграции

Убедитесь, что каждый уникальный ключ данных всегда соответствует одним и тем же данным. Для данных, привязанных к странице, задавайте ключ, однозначно идентифицирующий эти данные. (useFetch делает это автоматически.)

app/pages/test/[slug].vue
// Так делать небезопасно на динамической странице: slug влияет на данные,
// но Nuxt этого не видит, если ключ не отражает slug.
const route = useRoute()
const { data } = await useAsyncData(async () => {
  return await $fetch(`/api/my-page/${route.params.slug}`)
})
// Используйте ключ, однозначно идентифицирующий загружаемые данные.
const { data } = await useAsyncData(route.params.slug, async () => {
  return await $fetch(`/api/my-page/${route.params.slug}`)
})

Отключить возможность можно так:

nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    sharedPrerenderData: false,
  },
})

Значения по умолчанию для data и error в useAsyncData и useFetch

🚦 Уровень влияния: минимальный

Что изменилось

Объекты data и error, возвращаемые useAsyncData, по умолчанию теперь undefined.

Причины изменений

Раньше data инициализировался как null, а в clearNuxtData сбрасывался в undefined; error был null. Изменение для единообразия.

Шаги миграции

Проверки на data.value === null или error.value === null замените на проверку undefined.

Автоматизировать: npx codemod@latest nuxt/4/default-data-error-value

Удаление устаревших boolean-значений опции dedupe при вызове refresh в useAsyncData и useFetch

🚦 Уровень влияния: минимальный

Что изменилось

Раньше в refresh можно было передать dedupe: boolean — это были алиасы cancel (true) и defer (false).

app/app.vue
// @errors: 2322
const { refresh } = await useAsyncData(() => Promise.resolve({ message: 'Hello, Nuxt!' }))

async function refreshData () {
  await refresh({ dedupe: true })
}

Причины изменений

Алиасы убраны для ясности: при добавлении dedupe в опции useAsyncData boolean-значения оказались противоположными по смыслу.

refresh({ dedupe: false }) означало «не отменять текущие запросы в пользу нового», а dedupe: true в опциях useAsyncData — «не делать новый запрос, если уже есть ожидающий». См. PR.

Шаги миграции

Достаточно заменить значения:

  const { refresh } = await useAsyncData(async () => ({ message: 'Hello, Nuxt 3!' }))
  
  async function refreshData () {
-   await refresh({ dedupe: true })
+   await refresh({ dedupe: 'cancel' })

-   await refresh({ dedupe: false })
+   await refresh({ dedupe: 'defer' })
  }
Автоматизировать: npx codemod@latest nuxt/4/deprecated-dedupe-value

Учёт значений по умолчанию при очистке data в useAsyncData и useFetch

🚦 Уровень влияния: минимальный

Что изменилось

При заданном кастомном default в useAsyncData при вызове clear или clearNuxtData данные сбрасываются в это значение по умолчанию, а не просто в «не задано».

Причины изменений

Часто задают пустое значение (например пустой массив), чтобы не проверять null/undefined при итерации. При сбросе/очистке это значение должно восстанавливаться.

Согласование значения pending в useAsyncData и useFetch

🚦 Уровень влияния: средний

pending, возвращаемый useAsyncData, useFetch, useLazyAsyncData и useLazyFetch, теперь computed: true только когда status тоже pending.

Что изменилось

При immediate: false теперь pending будет false, пока не выполнен первый запрос. Раньше до первого запроса pending был всегда true.

Причины изменений

Смысл pending приведён в соответствие со свойством status, которое тоже в состоянии pending во время запроса.

Шаги миграции

Если вы опираетесь на pending, учтите: теперь он true только при status === 'pending'.

  <template>
-   <div v-if="!pending">
+   <div v-if="status === 'success'">
      <p>Data: {{ data }}</p>
    </div>
    <div v-else>
      <p>Loading...</p>
    </div>
  </template>
  <script setup lang="ts">
  const { data, pending, execute, status } = await useAsyncData(() => fetch('/api/data'), {
    immediate: false
  })
  onMounted(() => execute())
  </script>

Временно вернуть прежнее поведение можно так:

nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    pendingWhenIdle: true,
  },
})

Key Change Behavior in useAsyncData and useFetch

🚦 Уровень влияния: средний

Что изменилось

При реактивных ключах в useAsyncData или useFetch Nuxt автоматически перезапрашивает данные при смене ключа. При immediate: false данные запрашиваются при смене ключа только если они уже были запрошены хотя бы раз.

Раньше useFetch вёл себя иначе: всегда запрашивал данные при смене ключа.

Теперь useFetch и useAsyncData ведут себя одинаково: при смене ключа данные запрашиваются только если они уже были получены ранее.

Причины изменений

Единое поведение и отсутствие неожиданных запросов. При immediate: false нужно вызывать refresh или execute, иначе данные в useFetch/useAsyncData не будут запрошены.

Шаги миграции

Обычно поведение только улучшается. Если вы полагались на автоматический запрос при смене ключа/опций у не-immediate useFetch, первый запрос теперь нужно запускать вручную.

  const id = ref('123')
  const { data, execute } = await useFetch('/api/test', {
    query: { id },
    immediate: false
  )
+ watch(id, () => execute(), { once: true })

Отключить это поведение:

// Или глобально в nuxt.config
export default defineNuxtConfig({
  experimental: {
    alwaysRunFetchOnKeyChange: true,
  },
})

Shallow Data Reactivity in useAsyncData and useFetch

🚦 Уровень влияния: минимальный

data, возвращаемый useAsyncData, useFetch, useLazyAsyncData и useLazyFetch, теперь shallowRef, а не ref.

Что изменилось

При новом запросе зависимости от data остаются реактивными — объект заменяется целиком. Изменение свойств внутри этого объекта не вызовет реактивности в приложении.

Причины изменений

Существенный прирост производительности для вложенных объектов и массивов: Vue не отслеживает каждое свойство. В большинстве случаев data должен быть неизменяемым.

Шаги миграции

Обычно миграция не нужна. Если вы полагаетесь на глубокую реактивность data, есть два варианта:

  1. Включить глубокую реактивность точечно для каждого композабла:
    - const { data } = useFetch('/api/test')
    + const { data } = useFetch('/api/test', { deep: true })
    
  2. Задать поведение по умолчанию для всего проекта (не рекомендуется):
    nuxt.config.ts
    export default defineNuxtConfig({
      experimental: {
        defaults: {
          useAsyncData: {
            deep: true,
          },
        },
      },
    })
    
Автоматизировать: npx codemod@latest nuxt/4/shallow-function-reactivity

Absolute Watch Paths in builder:watch

🚦 Уровень влияния: минимальный

Что изменилось

Хук Nuxt builder:watch теперь передаёт абсолютный путь вместо относительного к srcDir.

Причины изменений

Так можно отслеживать пути вне srcDir и лучше поддерживать слои и сложные схемы.

Шаги миграции

Публичные модули Nuxt, использующие этот хук, уже обновлены. См. issue #25339.

Если вы автор модуля и хотите совместимость с Nuxt v3 и v4, используйте такой код:

+ import { relative, resolve } from 'node:fs'
  // ...
  nuxt.hook('builder:watch', async (event, path) => {
+   path = relative(nuxt.options.srcDir, resolve(nuxt.options.srcDir, path))
    // ...
  })
Автоматизировать: npx codemod@latest nuxt/4/absolute-watch-path

Удаление объекта window.__NUXT__

Что изменилось

Глобальный объект window.__NUXT__ удаляется после гидрации приложения.

Причины изменений

Это открывает путь к мульти-приложениям (#21635) и единому способу доступа к данным — useNuxtApp().

Шаги миграции

Данные по-прежнему доступны через useNuxtApp().payload:

- console.log(window.__NUXT__)
+ console.log(useNuxtApp().payload)

Сканирование index в каталогах

🚦 Уровень влияния: средний

Что изменилось

Вложенные папки в app/middleware/ тоже сканируются на наличие index-файлов, они регистрируются как middleware.

Причины изменений

Nuxt автоматически сканирует несколько папок, в том числе app/middleware/ и app/plugins/. В app/plugins/ уже сканировались вложенные папки по index — поведение приведено к единому виду.

Шаги миграции

Миграция обычно не нужна. Чтобы вернуть прежнее поведение, можно отфильтровать такие middleware хуком:

export default defineNuxtConfig({
  hooks: {
    'app:resolve' (app) {
      app.middleware = app.middleware.filter(mw => !/\/index\.[^/]+$/.test(mw.path))
    },
  },
})

Изменения компиляции шаблонов

🚦 Уровень влияния: минимальный

Что изменилось

Раньше Nuxt использовал lodash/template для компиляции шаблонов на файловой системе в формате .ejs.

Утилиты (serialize, importName, importSources) для генерации кода в этих шаблонах удаляются.

Причины изменений

В Nuxt v3 перешли на «виртуальный» синтаксис с функцией getContents(), гибче и быстрее.

У lodash/template были уязвимости; для Nuxt это не критично (сборка, доверенный код), но аудиты их показывают. Плюс lodash — тяжёлая зависимость, большинству проектов не нужна.

Функции сериализации кода лучше вынести в отдельные пакеты, например unjs/knitwork, чтобы исправления не требовали обновления Nuxt.

Шаги миграции

Модули на EJS уже обновляются через PR. Самостоятельно можно сделать так (с сохранением совместимости):

  • Перенести интерполяцию строк в getContents().
  • Реализовать свою подстановку, как в https://github.com/nuxt-modules/color-mode/pull/240.
  • Подключить es-toolkit/compat (замена lodash template) как зависимость вашего проекта:
+ import { readFileSync } from 'node:fs'
+ import { template } from 'es-toolkit/compat'
  // ...
  addTemplate({
    fileName: 'appinsights-vue.js'
    options: { /* some options */ },
-   src: resolver.resolve('./runtime/plugin.ejs'),
+   getContents({ options }) {
+     const contents = readFileSync(resolver.resolve('./runtime/plugin.ejs'), 'utf-8')
+     return template(contents)({ options })
+   },
  })

Утилиты шаблонов (serialize, importName, importSources) можно заменить функциями из knitwork:

import { genDynamicImport, genImport, genSafeVariableName } from 'knitwork'

const serialize = (data: any) => JSON.stringify(data, null, 2).replace(/"\{(.+)\}"(?=,?$)/gm, r => JSON.parse(r).replace(/^\{(.*)\}$/, '$1'))

const importSources = (sources: string | string[], { lazy = false } = {}) => {
  return toArray(sources).map((src) => {
    if (lazy) {
      return `const ${genSafeVariableName(src)} = ${genDynamicImport(src, { comment: `webpackChunkName: ${JSON.stringify(src)}` })}`
    }
    return genImport(src, genSafeVariableName(src))
  }).join('\n')
}

const importName = genSafeVariableName
Автоматизировать: npx codemod@latest nuxt/4/template-compilation-changes

Изменения конфигурации TypeScript по умолчанию

🚦 Уровень влияния: минимальный

Что изменилось

compilerOptions.noUncheckedIndexedAccess теперь true вместо false.

Причины изменений

Продолжение обновления конфига 3.12: приведение дефолтов в соответствие с рекомендациями TotalTypeScript.

Шаги миграции

Варианты:

  1. Запустить проверку типов и исправить новые ошибки (рекомендуется).
  2. Переопределить значение в nuxt.config.ts:
    export default defineNuxtConfig({
      typescript: {
        tsConfig: {
          compilerOptions: {
            noUncheckedIndexedAccess: false,
          },
        },
      },
    })
    

Разделение конфигурации TypeScript

🚦 Уровень влияния: минимальный

Что изменилось

Nuxt теперь генерирует отдельные конфиги TypeScript для разных контекстов:

  1. Новые файлы конфигурации:
    • .nuxt/tsconfig.app.json — код приложения (компоненты Vue, composables и т.д.)
    • .nuxt/tsconfig.server.json — серверный код (Nitro/server)
    • .nuxt/tsconfig.node.json — код сборки (модули, nuxt.config.ts и т.д.)
    • .nuxt/tsconfig.shared.json — общий код (типы, утилиты)
    • .nuxt/tsconfig.json — прежний конфиг для обратной совместимости
  2. Обратная совместимость: проекты, расширяющие .nuxt/tsconfig.json, работают как раньше.
  3. Опциональные project references: можно включить для улучшенной проверки типов.
  4. Проверка по контексту: у каждого контекста свои compiler options и includes/excludes.
  5. Опция typescript.nodeTsConfig: настройка TypeScript для Node.js при сборке.

Причины изменений

Плюсы:

  1. Типобезопасность: у контекстов (app, server, build-time) свои глобалы и API.
  2. Лучше в IDE: точнее IntelliSense и ошибки по частям кодовой базы.
  3. Разделение: серверный код не предлагает клиентские API и наоборот.
  4. Производительность: TypeScript эффективнее с правильно разграниченными конфигами.

Раньше auto-imports в nuxt.config.ts не помечались TypeScript; контекст server/ в IDE не совпадал с проверкой типов.

Шаги миграции

Миграция не обязательна — существующие проекты работают без изменений.

Чтобы использовать улучшенную проверку типов, включите project references:

  1. Обновите корневой tsconfig.json:
    Если в tsconfig.json есть "extends": "./.nuxt/tsconfig.json", удалите его перед добавлением references. Project references и extends несовместимы.
    {
      // Удалите "extends": "./.nuxt/tsconfig.json", если есть
      "files": [],
      "references": [
        { "path": "./.nuxt/tsconfig.app.json" },
        { "path": "./.nuxt/tsconfig.server.json" },
        { "path": "./.nuxt/tsconfig.shared.json" },
        { "path": "./.nuxt/tsconfig.node.json" }
      ]
    }
    
  2. Удалите ручные server/tsconfig.json, если они расширяли .nuxt/tsconfig.server.json.
  3. Обновите скрипты проверки типов — используйте флаг build для project references:
    - "typecheck": "nuxt prepare && vue-tsc --noEmit"
    + "typecheck": "nuxt prepare && vue-tsc -b --noEmit"
    
  4. Перенесите расширения типов в нужный контекст:
    • типы для app — в app/
    • типы для server — в server/
    • общие типы (app и server) — в shared/
    Расширения типов вне app/, server/ или shared/ не будут работать с новыми project references.
  5. При необходимости настройте опции TypeScript:
    export default defineNuxtConfig({
      typescript: {
        // customize tsconfig.app.json
        tsConfig: {
          // ...
        },
        // customize tsconfig.shared.json
        sharedTsConfig: {
          // ...
        },
        // customize tsconfig.node.json
        nodeTsConfig: {
          // ...
        },
      },
      nitro: {
        typescript: {
          // customize tsconfig.server.json
          tsConfig: {
            // ...
          },
        },
      },
    })
    
  6. Обновите CI/сборочные скрипты проверки типов под новый подход с project references.

Новая конфигурация даёт лучшую типобезопасность и IntelliSense при включении, без потери совместимости для существующих проектов.

Удаление экспериментальных опций

🚦 Уровень влияния: минимальный

Что изменилось

В Nuxt 4 эти опции больше не настраиваются:

  • experimental.treeshakeClientOnly — всегда true (дефолт с v3.0)
  • experimental.configSchema — всегда true (дефолт с v3.3)
  • experimental.polyfillVueUseHead — всегда false (дефолт с v3.4)
  • experimental.respectNoSSRHeader — всегда false (дефолт с v3.4)
  • vite.devBundler не настраивается — по умолчанию vite-node

Причины изменений

Значения давно зафиксированы, причин оставлять их конфигурируемыми нет.

Шаги миграции

Удаление конфигурации верхнего уровня generate

🚦 Уровень влияния: минимальный

Что изменилось

Опция конфигурации верхнего уровня generate в Nuxt 4 недоступна, в том числе:

  • generate.exclude — исключение маршрутов из пререндеринга
  • generate.routes — список маршрутов для пререндеринга

Причины изменений

generate остался с Nuxt 2. В Nuxt 3+ предпочтительный способ — nitro.prerender.

Шаги миграции

Замените generate на соответствующие опции nitro.prerender:

export default defineNuxtConfig({
- generate: {
-   exclude: ['/admin', '/private'],
-   routes: ['/sitemap.xml', '/robots.txt']
- }
+ nitro: {
+   prerender: {
+     ignore: ['/admin', '/private'],
+     routes: ['/sitemap.xml', '/robots.txt']
+   }
+ }
})
Подробнее о настройке prerender в Nitro.

Nuxt 2 и Nuxt 3+

Краткое сравнение трёх вариантов Nuxt:

Функция / ВерсияNuxt 2Nuxt BridgeNuxt 3+
Vue223
Стабильность😊 Стабильно😊 Стабильно😊 Стабильно
Производительность🏎 Быстро✈️ Быстрее🚀 Быстрее всего
Движок Nitro
Поддержка ESM🌙 Частично👍 Лучше
TypeScript☑️ Опционально🚧 Частично
Composition API🚧 Частично
Options API
Автоимпорт компонентов
Синтаксис <script setup>🚧 Частично
Автоимпорты
webpack445
Vite⚠️ Частично🚧 Частично
Nuxt CLI❌ Старый✅ nuxt✅ nuxt
Статические сайты

Миграция с Nuxt 2 на Nuxt 3+

В руководстве по миграции — пошаговое сравнение возможностей Nuxt 2 и Nuxt 3+ и адаптация приложения.

Руководство по миграции с Nuxt 2 на Nuxt 3

Nuxt 2 и Nuxt Bridge

Для постепенной миграции с Nuxt 2 на Nuxt 3 можно использовать Nuxt Bridge — слой совместимости, позволяющий подключать возможности Nuxt 3+ в Nuxt 2 по мере необходимости.

Миграция с Nuxt 2 на Nuxt Bridge