Від Vue 3.0 до Vue 3.6: як змінився фреймворк за шість років

Матеріал актуальний станом на 11 серпня 2026 року. Поточна стабільна версія — Vue 3.5.41, а Vue 3.6 перебуває на стадії Release Candidate: останній доступний prerelease — 3.6.0-rc.3. Тому API Vue 3.6, особливо Vapor Mode, ще варто вважати передрелізними.

Коли Vue 3.0 вийшов у вересні 2020 року, це була не просто наступна версія Vue 2. Фреймворк отримав переписане ядро, нову систему реактивності на Proxy, Composition API, кращу підтримку TypeScript і компілятор, який передавав рантайму більше інформації про шаблон.

Проте Vue 3.0 був лише фундаментом. За наступні мінорні релізи команда суттєво змінила спосіб написання компонентів, зробила TypeScript майже безшовним, кілька разів оптимізувала реактивність, додала керовану гідратацію для SSR і, нарешті, підготувала альтернативний режим рендерингу без Virtual DOM — Vapor Mode.

Ця стаття охоплює головні публічні можливості, які впливають на прикладний код. Вона не перераховує кожне виправлення з changelog, але включає ключові API, синтаксичні зміни, оптимізації та важливі обмеження кожної версії.

Коротка хронологія

  • Vue 3.0, вересень 2020 року. Нова архітектура, Proxy-реактивність, Composition API, Fragments і Teleport.

  • Vue 3.1, червень 2021 року. Міграційний міст @vue/compat, налаштування компілятора та подальше дозрівання <script setup>.

  • Vue 3.2, серпень 2021 року. Стабільний <script setup>, CSS v-bind(), Web Components, effectScope і v-memo.

  • Vue 3.3, травень 2023 року. Велике покращення TypeScript DX: generic-компоненти, типізовані слоти й події, defineOptions і перша версія defineModel.

  • Vue 3.4, грудень 2023 року. Швидший парсер і точніша реактивність, стабільний defineModel, одноразові watchers.

  • Vue 3.5, вересень 2024 року. Реактивна деструктуризація props, lazy hydration, useId, useTemplateRef і нові можливості watchers.

  • Vue 3.6, Release Candidate у серпні 2026 року. Vapor Mode та нове внутрішнє ядро реактивності на основі alien-signals.

Vue 3.0: фундамент сучасного Vue

Vue 3.0 «One Piece» вийшов 18 вересня 2020 року. Саме тут з’явилася більшість концепцій, які ми сьогодні сприймаємо як звичайний Vue. Офіційний анонс Vue 3.0.

Composition API

У Vue 2 логіку компонента переважно розкладали за опціями data, computed, methods і watch. У великому компоненті код однієї функціональності міг опинитися у кількох віддалених секціях.

Composition API дозволив групувати код за предметною задачею та виносити реактивну логіку у composable-функції.

У Vue 3.0 <script setup> ще був експериментальним, тому типовий компонент виглядав так:

<script lang="ts">
import {
  computed,
  defineComponent,
  onMounted,
  ref
} from 'vue'

export default defineComponent({
  setup() {
    const count = ref(0)

    const doubled = computed(
      () => count.value * 2
    )

    function increment() {
      count.value++
    }

    onMounted(() => {
      console.log('Компонент змонтовано')
    })

    return {
      count,
      doubled,
      increment
    }
  }
})
</script>

<template>
  <button @click="increment">
    {{ count }} × 2 = {{ doubled }}
  </button>
</template>

У JavaScript-коді значення ref читається через .value, а в шаблоні Vue автоматично його розгортає.

Найважливішим наслідком Composition API стали composables — звичайні функції, які інкапсулюють і повторно використовують реактивну логіку:

import {
  onMounted,
  onUnmounted,
  ref
} from 'vue'

export function useOnlineStatus() {
  const isOnline = ref(navigator.onLine)

  const update = () => {
    isOnline.value = navigator.onLine
  }

  onMounted(() => {
    window.addEventListener(
      'online',
      update
    )

    window.addEventListener(
      'offline',
      update
    )
  })

  onUnmounted(() => {
    window.removeEventListener(
      'online',
      update
    )

    window.removeEventListener(
      'offline',
      update
    )
  })

  return { isOnline }
}

Це стало масштабованішою альтернативою mixins: залежності та результат функції видно явно, немає конфліктів імен і значно краще працює виведення типів.

Реактивність на основі Proxy

Vue 2 використовував Object.defineProperty, через що мав відомі обмеження: фреймворк не міг природно відстежувати додавання й видалення властивостей, а Map і Set взагалі не були повноцінними реактивними колекціями.

Vue 3 перейшов на Proxy:

import {
  reactive,
  watchEffect
} from 'vue'

const state = reactive({
  filters: new Map<string, string>()
})

watchEffect(() => {
  console.log(
    state.filters.get('status')
  )
})

state.filters.set(
  'status',
  'active'
)

state.filters.delete('status')

Операції з об’єктами, масивами, Map, Set, WeakMap і WeakSet стали частиною однієї системи реактивності.

Зворотний бік — Vue 3 відмовився від підтримки Internet Explorer, де Proxy неможливо повноцінно поліфілити.

Окремий екземпляр застосунку через createApp

У Vue 2 глобальні виклики на кшталт Vue.use() або Vue.component() змінювали спільний глобальний стан.

У Vue 3 конфігурація належить конкретному застосунку:

import { createApp } from 'vue'
import App from './App.vue'
import BaseButton from './components/BaseButton.vue'

const app = createApp(App)

app.component(
  'BaseButton',
  BaseButton
)

app.provide(
  'apiBaseUrl',
  '/api'
)

app.mount('#app')

Це спростило:

  • ізольоване тестування;
  • microfrontend-сценарії;
  • одночасне монтування кількох Vue-застосунків;
  • використання різних плагінів і конфігурацій на одній сторінці.

Fragments: кілька кореневих вузлів

Компоненту більше не потрібен зайвий wrapper-div:

<template>
  <header>
    Шапка
  </header>

  <main v-bind="$attrs">
    Основний вміст
  </main>

  <footer>
    Підвал
  </footer>
</template>

Якщо компонент має кілька коренів, потрібно явно вирішити, куди передавати fallthrough-атрибути через v-bind="$attrs".

Teleport

<Teleport> дозволив залишити логіку модального вікна всередині компонента, але фізично відрендерити DOM в іншому місці — наприклад, безпосередньо в body:

<Teleport to="body">
  <div
    v-if="isOpen"
    class="modal"
    role="dialog"
  >
    <button @click="isOpen = false">
      Закрити
    </button>
  </div>
</Teleport>

Це розв’язує проблеми з overflow, z-index і stacking context, не руйнуючи логічне дерево компонентів.

Новий контракт v-model

Для компонентів стандартна пара змінилася з value + input на modelValue + update:modelValue.

Водночас з’явилися аргументи, власні модифікатори й кілька v-model на одному компоненті:

<UserName
  v-model:first-name="firstName"
  v-model:last-name="lastName"
/>

Дочірній компонент міг реалізовувати цей контракт так:

<script>
import { defineComponent } from 'vue'

export default defineComponent({
  props: {
    firstName: String,
    lastName: String
  },

  emits: [
    'update:firstName',
    'update:lastName'
  ]
})
</script>

<template>
  <input
    :value="firstName"
    @input="$emit(
      'update:firstName',
      $event.target.value
    )"
  >

  <input
    :value="lastName"
    @input="$emit(
      'update:lastName',
      $event.target.value
    )"
  >
</template>

Окрема опція emits документує події, дозволяє їх валідувати та не дає оголошеним подіям випадково потрапляти в DOM як listeners.

Compiler-informed Virtual DOM і tree-shaking

Vue 3 не відмовився від Virtual DOM, але компілятор почав позначати динамічні частини шаблону через patch flags, піднімати статичні вузли та формувати block tree.

Рантайм порівнював не все дерево, а переважно ті вузли, які справді могли змінитися.

Внутрішні пакети також стали модульнішими. Невикористані API можна прибрати під час tree-shaking, а @vue/reactivity — застосовувати окремо від DOM-рендерера.

До базового набору Vue 3.0 також увійшли:

  • краща TypeScript-архітектура — саме ядро переписали на TypeScript;
  • API для створення власних renderers;
  • компонент <Suspense> для асинхронних залежностей, хоча він довго залишався експериментальним;
  • покращені async components і функція defineAsyncComponent;
  • повністю tree-shakable глобальні API.

Vue 3.1: міст для міграції з Vue 2

Vue 3.1 не намагався змінити щоденний синтаксис так само радикально, як 3.0. Його головним результатом став Migration Build — пакет @vue/compat. Офіційний changelog Vue 3.1.

@vue/compat

Compat build запускає Vue 3 з частиною поведінки Vue 2 і показує попередження там, де код потрібно оновити.

Режим можна перемикати глобально або для окремого компонента:

import { configureCompat } from 'vue'

configureCompat({
  MODE: 2
})

У цьому режимі поведінка максимально наближена до Vue 2.

Окремий компонент можна перевести в чистий режим Vue 3:

export default {
  compatConfig: {
    MODE: 3
  }
}

Це дозволяло мігрувати великі програми поступово, а не переписувати все одним релізом.

Налаштування компілятора

З’явилися app.config.compilerOptions і component-level compilerOptions.

Наприклад, у runtime compiler можна описати власні HTML-елементи:

app.config.compilerOptions.isCustomElement = (
  tag
) => {
  return tag.startsWith('x-')
}

У проєктах, де шаблони компілюються під час збірки, аналогічну опцію потрібно передавати плагіну компілятора, а не рантайму.

SSR і розвиток <script setup>

У 3.1 з’явився Composition API hook onServerPrefetch:

import {
  onServerPrefetch,
  ref
} from 'vue'

const article =
  ref<Article | null>(null)

onServerPrefetch(async () => {
  article.value =
    await fetchArticle()
})

Упродовж гілки 3.1 експериментальний <script setup> отримав:

  • defineExpose;
  • withDefaults;
  • useAttrs;
  • useSlots;
  • кращу підтримку типів;
  • відновлення component context після top-level await.

Стабільним цей формат став лише у Vue 3.2.

Vue 3.2: сучасний формат Single-File Components

Vue 3.2 — версія, після якої сучасний Vue-код почав виглядати знайомо: <script setup> та динамічний CSS стали стабільними. Офіційний анонс Vue 3.2.

Стабільний <script setup>

Порівняймо один компонент до і після.

Звичайний setup():

<script lang="ts">
import {
  defineComponent,
  ref
} from 'vue'

export default defineComponent({
  props: {
    initial: {
      type: Number,
      default: 0
    }
  },

  emits: ['change'],

  setup(props, { emit }) {
    const count =
      ref(props.initial)

    const increment = () => {
      count.value++

      emit(
        'change',
        count.value
      )
    }

    return {
      count,
      increment
    }
  }
})
</script>

Той самий компонент у Vue 3.2+:

<script setup lang="ts">
import { ref } from 'vue'

const props = withDefaults(
  defineProps<{
    initial?: number
  }>(),
  {
    initial: 0
  }
)

const emit = defineEmits<{
  (
    event: 'change',
    value: number
  ): void
}>()

const count =
  ref(props.initial)

function increment() {
  count.value++

  emit(
    'change',
    count.value
  )
}
</script>

<template>
  <button @click="increment">
    {{ count }}
  </button>
</template>

Верхньорівневі змінні й імпортовані компоненти одразу доступні шаблону.

Не потрібно:

  • створювати export default;
  • викликати defineComponent;
  • вручну повертати змінні із setup();
  • окремо реєструвати імпортовані компоненти.

Реактивні CSS-значення через v-bind()

<script setup lang="ts">
import { ref } from 'vue'

const accent = ref('#42b883')

function toggleColor() {
  accent.value =
    accent.value === '#42b883'
      ? '#35495e'
      : '#42b883'
}
</script>

<template>
  <button @click="toggleColor">
    Змінити колір
  </button>
</template>

<style scoped>
button {
  color: v-bind(accent);
}
</style>

Компілятор перетворює значення на CSS custom properties, тому стилі залишаються реактивними й сумісними зі scoped CSS.

defineCustomElement

Vue-компонент стало простіше упакувати як нативний Custom Element і використати без Vue-застосунку або всередині іншого фреймворку:

import {
  defineCustomElement
} from 'vue'

import UserBadge from './UserBadge.ce.vue'

const UserBadgeElement =
  defineCustomElement(UserBadge)

customElements.define(
  'user-badge',
  UserBadgeElement
)

Після реєстрації браузер розуміє <user-badge> як звичайний Web Component.

effectScope

effectScope групує computed, watchers та інші реактивні effects, щоб зупинити їх разом.

Це особливо корисно авторам бібліотек:

import {
  effectScope,
  ref,
  watchEffect
} from 'vue'

const scope = effectScope()

scope.run(() => {
  const count = ref(0)

  watchEffect(() => {
    console.log(count.value)
  })
})

scope.stop()

Виклик scope.stop() зупиняє всі effects, створені всередині scope.

v-memo

Директива дозволяє пропустити створення й порівняння піддерева, доки масив залежностей не змінився:

<article
  v-for="item in items"
  :key="item.id"
  v-memo="[
    item.id === selectedId
  ]"
>
  <h3>{{ item.title }}</h3>

  <ExpensivePreview
    :item="item"
  />
</article>

Це вузька оптимізація для дуже великих списків або дорогих піддерев. У звичайних компонентах компілятор Vue вже робить достатньо оптимізацій без v-memo.

Інші зміни Vue 3.2

До помітних нововведень також належали:

  • watchPostEffect() як скорочення для watchEffect із flush: 'post';
  • watchSyncEffect() як варіант із flush: 'sync';
  • модифікатори .prop і .attr для v-bind;
  • ESM-версія server renderer без жорсткої залежності від Node.js;
  • підтримка Web Streams API для SSR;
  • суттєві оптимізації ref, dependency tracking, пам’яті й створення VNodes.

Vue 3.3: TypeScript стає першокласним сценарієм

Vue 3.3 був сфокусований не стільки на нових візуальних можливостях, скільки на developer experience у <script setup lang="ts">. Офіційний анонс Vue 3.3.

Імпортовані та складні типи в макросах

Раніше generic-аргумент defineProps переважно мав бути локальним type literal або interface.

У 3.3 компілятор навчився розв’язувати імпортовані типи, intersections і частину utility types:

import type {
  BaseCardProps
} from './types'

const props = defineProps<
  BaseCardProps & {
    featured?: boolean
  }
>()

Аналіз залишається AST-based, а не повною перевіркою TypeScript.

Тому умовний тип для всього об’єкта props може не підтримуватися, хоча conditional type окремої властивості допустимий.

Generic-компоненти

<script
  setup
  lang="ts"
  generic="T extends {
    id: string | number
  }"
>
const props = defineProps<{
  items: T[]
}>()

const emit = defineEmits<{
  select: [item: T]
}>()

defineSlots<{
  default?: (
    props: { item: T }
  ) => unknown
}>()
</script>

<template>
  <button
    v-for="item in props.items"
    :key="item.id"
    @click="emit('select', item)"
  >
    <slot :item="item" />
  </button>
</template>

Тип конкретного T виводиться з переданого items, а слот і подія отримують той самий тип.

До 3.3 подібні компоненти вимагали складних обхідних рішень.

Коротший тип defineEmits

Було:

const emit = defineEmits<{
  (
    event: 'save',
    id: number
  ): void

  (
    event: 'cancel'
  ): void
}>()

Стало:

const emit = defineEmits<{
  save: [id: number]
  cancel: []
}>()

Старий call-signature формат не прибрали — новий tuple-синтаксис лише зробив декларації компактнішими.

defineSlots

defineSlots описує доступні слоти та їхні slot props для IDE і vue-tsc:

defineSlots<{
  default?: (
    props: { user: User }
  ) => unknown

  empty?: () => unknown
}>()

Це не змінює runtime-поведінку компонента. Макрос насамперед передає інформацію системі типів та інструментам розробки.

defineOptions

defineOptions прибрав потребу додавати другий <script> лише заради опцій компонента:

<script setup lang="ts">
defineOptions({
  name: 'SearchField',
  inheritAttrs: false
})
</script>

defineModel — поки експериментальний

У 3.3 вперше з’явився макрос defineModel:

<script setup lang="ts">
const model =
  defineModel<string>()
</script>

<template>
  <input v-model="model">
</template>

Він автоматично:

  • оголошує prop modelValue;
  • оголошує подію update:modelValue;
  • повертає синхронізований ref.

У Vue 3.3 функцію потрібно було окремо ввімкнути в налаштуваннях SFC-компілятора. Стабільною вона стала у 3.4.

Так само експериментальною була реактивна деструктуризація defineProps. За замовчуванням її ввімкнули лише у Vue 3.5.

Нормалізація джерел через toRef() і toValue()

toValue() розгортає:

  • звичайне значення;
  • ref;
  • getter-функцію.

Це дозволяє composable-функціям приймати всі три форми без втрати реактивності:

import {
  ref,
  toValue,
  watchEffect,
  type Ref
} from 'vue'

type ValueSource<T> =
  | T
  | Ref<T>
  | (() => T)

export function useDocumentTitle(
  source: ValueSource<string>
) {
  watchEffect(() => {
    document.title =
      toValue(source)
  })
}

useDocumentTitle('Профіль')

useDocumentTitle(
  () => `Профіль: ${props.userName}`
)

const sectionTitle =
  ref('Налаштування')

useDocumentTitle(sectionTitle)

toRef() також навчився приймати getter і повертати readonly ref, який обчислює значення під час читання.

Інші можливості Vue 3.3

Серед менш видимих, але корисних API були:

  • app.runWithContext() для виконання функції в injection context застосунку;
  • hasInjectionContext() для перевірки контексту всередині бібліотек;
  • підтримка jsxImportSource, щоб типи Vue JSX не конфліктували з React;
  • generic-підтримка у defineComponent;
  • покращена робота Suspense через опцію suspensible.

Vue 3.4: менше зайвої роботи — і в коді, і в рантаймі

Vue 3.4 приніс великий внутрішній рефакторинг і кілька невеликих API, які помітно спростили прикладний код. Офіційний анонс Vue 3.4.

Новий парсер шаблонів

Команда переписала parser як state-machine tokenizer з одним проходом по шаблону.

В офіційних бенчмарках він працював приблизно вдвічі швидше, а компіляція script + template частин SFC із source maps покращилась приблизно на 44%.

У реальному build приріст менший, бо Vue compiler — лише одна частина всього pipeline. Проте виграли також Volar, vue-tsc і плагіни, що аналізують SFC.

Точніша реактивність computed

До 3.4 залежний effect міг запускатися після кожної зміни джерела, навіть якщо результат computed залишився тим самим:

import {
  computed,
  ref,
  watchEffect
} from 'vue'

const count = ref(0)

const isEven = computed(
  () => count.value % 2 === 0
)

watchEffect(() => {
  console.log(isEven.value)
})

count.value = 2

Починаючи з 3.4, effect не запускається повторно в цьому прикладі: isEven був true і залишився true.

Це зменшує зайві обчислення та ререндери.

Крім того:

  • синхронний effect запускається лише один раз, якщо одночасно змінилося кілька його computed-залежностей;
  • shift, unshift і splice більше не спричиняють серію зайвих синхронних запусків.

Стабільний defineModel

У Vue 3.4 макрос став стабільним і отримав кращу підтримку modifiers та transformers:

<!-- TitleInput.vue -->
<script setup lang="ts">
const title =
  defineModel<string>(
    'title',
    {
      required: true
    }
  )
</script>

<template>
  <input v-model="title">
</template>

Батьківський компонент:

<TitleInput
  v-model:title="pageTitle"
/>

Те, що у Vue 3.0 вимагало ручної пари defineProps + defineEmits, тепер описується одним макросом.

Скорочений v-bind

Якщо назва атрибута й змінної однакова, можна не дублювати її:

<!-- До Vue 3.4 -->
<img
  :id="id"
  :src="src"
  :alt="alt"
>

<!-- Vue 3.4+ -->
<img :id :src :alt>

Одноразовий watcher

watch(
  userId,
  loadUser,
  {
    once: true
  }
)

Callback виконається під час першої зміни userId, після чого watcher автоматично зупиниться.

Попереднє значення в computed

Getter може отримати попередній результат.

Наприклад, значення «залипає» після перевищення ліміту:

const count = ref(0)

const limited = computed(
  (previous) => {
    if (count.value <= 3) {
      return count.value
    }

    return previous
  }
)

Краща діагностика SSR

Vue 3.4 покращив повідомлення про hydration mismatch:

  • повідомлення чіткіше розділяє серверне та клієнтське значення;
  • вказує проблемний DOM-вузол;
  • перевіряє class, style та інші динамічні атрибути;
  • дозволяє залишити докладну діагностику у production.

Для останнього з’явився compile-time flag:

__VUE_PROD_HYDRATION_MISMATCH_DETAILS__

Також команда додала офіційний довідник production error codes.

Інші зміни Vue 3.4

До релізу ввійшли:

  • підтримка MathML;
  • видалення експериментального Reactivity Transform;
  • припинення автоматичної реєстрації глобального namespace JSX;
  • покращене виведення типів подій і слотів;
  • нові публічні TypeScript-типи для компонентів та плагінів.

TSX-проєктам потрібно вказувати jsxImportSource: "vue" або використовувати vue/jsx для старої глобальної поведінки.

Це був type-only breaking change у мінорному релізі. Для SFC-проєктів з актуальними Vue Language Tools він зазвичай непомітний.

Vue 3.5: реактивні props, керована гідратація та зрілі watchers

У Vue 3.5 команда ще раз переписала внутрішні структури dependency tracking.

За офіційними вимірами це зменшило споживання пам’яті системою реактивності на 56%, а операції з великими глибоко реактивними масивами в окремих сценаріях прискорилися до десяти разів.

Публічна поведінка при цьому не змінилася. Офіційний анонс Vue 3.5.

Реактивна деструктуризація props за замовчуванням

До 3.5 деструктуризація звичайного props втрачала реактивний зв’язок.

Тепер компілятор переписує змінні, отримані безпосередньо з defineProps, у доступи до props:

<script setup lang="ts">
import { watch } from 'vue'

const {
  count = 0,
  message = 'Вітаю'
} = defineProps<{
  count?: number
  message?: string
}>()

watch(
  () => count,
  (value) => {
    console.log(
      'Нове значення:',
      value
    )
  }
)
</script>

<template>
  <p>
    {{ message }}: {{ count }}
  </p>
</template>

Дефолти задаються звичайним JavaScript-синтаксисом.

Але важливий нюанс: watch(count, ...) не працює як реактивне джерело, адже у виклик передається поточне число.

Потрібно передати getter:

watch(
  () => count,
  (value) => {
    console.log(value)
  }
)

Те саме стосується composables: вони мають приймати getter і нормалізувати його через toValue().

useTemplateRef

Раніше template ref оголошували як звичайний ref(null), а його ім’я мало збігатися з атрибутом у шаблоні.

Новий API робить зв’язок явним і краще типізується інструментами:

<script setup lang="ts">
import {
  onMounted,
  useTemplateRef
} from 'vue'

const searchInput =
  useTemplateRef<HTMLInputElement>(
    'search'
  )

onMounted(() => {
  searchInput.value?.focus()
})
</script>

<template>
  <input
    ref="search"
    type="search"
  >
</template>

Cleanup асинхронної роботи через onWatcherCleanup

import {
  onWatcherCleanup,
  ref,
  watch
} from 'vue'

const userId = ref(1)

watch(userId, async (id) => {
  const controller =
    new AbortController()

  onWatcherCleanup(() => {
    controller.abort()
  })

  const response = await fetch(
    `/api/users/${id}`,
    {
      signal: controller.signal
    }
  )

  console.log(
    await response.json()
  )
})

Коли userId зміниться до завершення запиту, Vue виконає cleanup і скасує застарілий fetch.

onWatcherCleanup() потрібно зареєструвати синхронно — до першого await.

Pause і resume для watchers

Watcher handle отримав додаткові методи:

const {
  pause,
  resume,
  stop
} = watch(
  source,
  callback
)

pause()
resume()
stop()

Це корисно, коли потрібно тимчасово припинити реакцію на зміни, не знищуючи watcher остаточно.

Числова глибина deep

Раніше глибокий watcher мав фактично два режими:

deep: false
deep: true

У Vue 3.5 deep може бути числом, яке визначає максимальну глибину обходу:

watch(
  settings,
  saveSettings,
  {
    deep: 2
  }
)

Це корисно, коли deep: true обходив би надто велику структуру.

Lazy hydration для SSR

Асинхронний компонент тепер може визначати, коли саме браузер має його гідратувати:

import {
  defineAsyncComponent,
  hydrateOnVisible
} from 'vue'

const AnalyticsChart =
  defineAsyncComponent({
    loader: () => import(
      './AnalyticsChart.vue'
    ),

    hydrate: hydrateOnVisible({
      rootMargin: '200px'
    })
  })

Компонент уже може бути присутнім у серверному HTML, але його JavaScript-логіка активується лише перед появою у viewport.

У Vue 3.5 є такі стратегії:

  • hydrateOnIdle() — коли браузер має вільний час;
  • hydrateOnVisible() — при наближенні до viewport;
  • hydrateOnMediaQuery() — коли виконується media query;
  • hydrateOnInteraction() — після заданої взаємодії користувача;
  • власна hydration strategy.

Стабільні ID між сервером і клієнтом через useId

<script setup lang="ts">
import { useId } from 'vue'

const id = useId()
</script>

<template>
  <label :for="id">
    Електронна пошта
  </label>

  <input
    :id
    type="email"
  >
</template>

ID:

  • унікальний у межах застосунку;
  • стабільний між SSR та клієнтом;
  • не спричиняє hydration mismatch;
  • підходить для форм і ARIA-зв’язків.

Контроль очікуваних hydration mismatches

Іноді сервер і браузер неминуче показують різні значення — наприклад, локалізовану дату.

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

<time data-allow-mismatch="text">
  {{ new Date().toLocaleString() }}
</time>

Допустимі категорії:

  • text;
  • children;
  • class;
  • style;
  • attribute.

Відкладений Teleport

До 3.5 target мав існувати на момент монтування <Teleport>.

Атрибут defer відкладає пошук target до завершення поточного render tick:

<Teleport
  defer
  to="#late-container"
>
  <p>
    Переміщений вміст
  </p>
</Teleport>

<div id="late-container"></div>

Інші можливості Vue 3.5

До релізу також увійшли:

  • можливість безпосередньо вкладати <Teleport> у <Transition>;
  • app.onUnmount() для реєстрації cleanup усього застосунку;
  • app.config.throwUnhandledErrorInProduction;
  • getCurrentWatcher();
  • onEffectCleanup();
  • опціональний failSilently у onScopeDispose();
  • useHost() і useShadowRoot() для Custom Elements;
  • shadowRoot: false;
  • nonce для стилів Custom Elements;
  • configureApp для окремого налаштування Custom Element;
  • сумісність із Trusted Types.

Vue 3.6: Vapor Mode змінює модель рендерингу

Vue 3.6 — найбільш концептуально важливий реліз після 3.0, але станом на 11 серпня 2026 року це ще 3.6.0-rc.3, а не стабільна версія. Офіційний prerelease Vue 3.6.0-rc.3.

Головних змін дві:

  1. новий opt-in режим компіляції Vapor Mode;
  2. великий внутрішній рефакторинг @vue/reactivity на основі alien-signals.

Що таке Vapor Mode

У звичайному Vue зміна стану запускає render function, яка створює нові VNodes. Vue порівнює їх із попередніми VNodes і застосовує потрібні зміни до DOM.

У Vapor Mode компілятор наперед створює точкові реактивні прив’язки до DOM.

Для простого {{ count }} рантайму не потрібно щоразу будувати й порівнювати віртуальне дерево: реактивний effect оновлює конкретний текстовий вузол.

Отже, Vapor прагне дати:

  • менший baseline bundle, бо pure Vapor app не завантажує VDOM runtime;
  • менше проміжних об’єктів;
  • менше роботи під час оновлення;
  • fine-grained продуктивність, близьку за підходом до Solid або Svelte;
  • знайомі Vue SFC і Composition API.

Як увімкнути Vapor для компонента

<script setup vapor lang="ts">
import {
  computed,
  ref
} from 'vue'

const count = ref(0)

const doubled = computed(
  () => count.value * 2
)
</script>

<template>
  <button @click="count++">
    {{ count }} × 2 = {{ doubled }}
  </button>
</template>

Також підтримуються скорочений <script vapor> і маркер на шаблоні:

<template vapor>
  <!-- ... -->
</template>

Pure Vapor application

Якщо весь застосунок складається з Vapor-компонентів, використовується інша точка входу:

import {
  createVaporApp
} from 'vue'

import App from './App.vue'

createVaporApp(App)
  .mount('#app')

У такому разі до bundle не потрапляє звичайний VDOM runtime.

Спільне використання Vapor і звичайних компонентів

Для поступового впровадження є interop plugin:

import {
  createApp,
  vaporInteropPlugin
} from 'vue'

import App from './App.vue'

createApp(App)
  .use(vaporInteropPlugin)
  .mount('#app')

Vapor- і VDOM-компоненти можна вкладати один в одного, передавати props, events і slots.

Проте interop додає звичайний VDOM runtime назад до bundle, а змішане глибоке вкладення ще може мати edge cases.

Команда Vue радить створювати чіткі «зони» одного режиму, а не безсистемно чергувати Vapor і VDOM у кожному компоненті.

Render functions і JSX залишаються VDOM-компонентами та потребують interop.

Поточні обмеження Vapor

У RC Vapor Mode не підтримує або свідомо не надає частину API, пов’язану з екземпляром компонента чи VNodes:

  • Options API;
  • app.config.globalProperties;
  • повноцінний component public instance;
  • getCurrentInstance() повертає null;
  • v-memo;
  • element lifecycle events виду @vue:mounted;
  • властивості $el, $props, $attrs, $slots і $refs через component template refs.

Custom directives у Vapor також мають інший низькорівневий контракт.

Значення передається як реактивний getter, а директива може повернути cleanup-функцію:

const myDirective = (
  element,
  source
) => {
  watchEffect(() => {
    element.textContent =
      source()
  })

  return () => {
    console.log('cleanup')
  }
}

Event delegation у поточному RC

На ранніх beta/RC етапах Vapor делегував підтримувані події до document автоматично.

Починаючи з RC.2, звичайні listeners знову монтуються безпосередньо на елемент, щоб поведінка збігалася з VDOM і нативним DOM.

Делегування тепер вмикається явно для конкретної статичної події через Vapor-only modifier:

<button @click.delegate="increment">
  Додати
</button>

Ця деталь добре показує, чому RC не слід описувати як завершений стабільний контракт: навіть під час release candidate окремі рішення ще можуть коригуватися.

Нова внутрішня реактивність

Друга частина Vue 3.6 — перенесення ідей alien-signals у @vue/reactivity.

Публічні API залишаються знайомими:

ref()
reactive()
computed()
watch()
watchEffect()

Але внутрішній граф залежностей став швидшим і економнішим за пам’яттю.

Тобто покращення отримує і класичний VDOM-режим, а не лише Vapor.

Чи варто вже використовувати Vue 3.6

Для production-проєкту без конкретної потреби краще залишатися на актуальній Vue 3.5 до стабільного релізу 3.6.

Vapor RC доречно тестувати у двох сценаріях, які прямо називає команда Vue:

  • окрема performance-sensitive сторінка або зона чинного застосунку;
  • невеликий новий застосунок, повністю написаний у Vapor Mode.

Масово переносити чинну кодову базу на Vapor лише заради нового синтаксичного атрибута поки не варто.

Перевага залежить від профілю застосунку, а екосистема компонентів може ще спиратися на VDOM-specific API.

Vue 3.0 проти сучасного Vue: різниця в щоденному коді

Опис компонента

У Vue 3.0 типовим підходом був setup() із ручним поверненням усіх змінних і методів, які потрібні шаблону.

У сучасному Vue використовується стабільний <script setup>, де верхньорівневі декларації одразу доступні в шаблоні.

TypeScript і props

У Vue 3.0 типи props здебільшого мали бути локальними та відносно простими.

У Vue 3.3+ підтримуються імпортовані типи, intersections, частина utility types і generic-компоненти.

Значення props за замовчуванням

У ранньому Vue 3 використовували runtime declaration або withDefaults.

У Vue 3.5 можна застосовувати реактивну деструктуризацію зі звичайними JavaScript defaults.

Типізація слотів

У Vue 3.0 типізація слотів значною мірою залежала від зовнішніх tooling-рішень.

У Vue 3.3+ для цього є defineSlots().

Компонентний v-model

У Vue 3.0 потрібно було вручну оголошувати prop і відповідну подію update:*.

У Vue 3.4+ цей контракт створюється через стабільний defineModel().

Template refs

У ранньому Vue 3 використовували ref(null), а назва змінної мала неявно збігатися з ref у шаблоні.

У Vue 3.5 з’явився useTemplateRef() із кращою інтеграцією з IDE та TypeScript.

Watchers

У Vue 3.0 cleanup передавався callback-аргументом.

У Vue 3.5 також доступні onWatcherCleanup(), pause(), resume() і числова глибина deep.

SSR hydration

У ранньому Vue 3 компоненти переважно гідратувалися одразу.

У Vue 3.5 з’явилися lazy hydration strategies, useId() і контрольоване придушення очікуваних hydration mismatches.

Teleport

У Vue 3.0 target мав існувати під час монтування.

У Vue 3.5 його пошук можна відкласти через defer.

Рендеринг

Vue 3.0 використовував compiler-informed Virtual DOM.

Vue 3.6 додає можливість обрати opt-in Vapor Mode, який у pure Vapor application не потребує VDOM runtime.

Що використовувати в новому проєкті сьогодні

Практична конфігурація без експериментів виглядає так:

  • актуальна стабільна Vue 3.5;
  • <script setup lang="ts">;
  • Composition API;
  • composables для повторного використання логіки;
  • reactive props destructure для простих defaults;
  • defineModel для компонентів із v-model;
  • useTemplateRef замість неявного збігу звичайного ref із шаблоном;
  • onWatcherCleanup для скасування запитів та інших side effects;
  • SSR hydration strategies лише там, де вони справді зменшують JavaScript на критичному шляху.

Vue 3.6 і Vapor варто тримати в полі зору та тестувати на реальних bottlenecks.

Але це не новий обов’язковий стиль Vue і не майбутня заміна всіх звичайних компонентів одним натисканням перемикача.

Висновок

Vue 3.0 визначив архітектуру: Proxy-реактивність, Composition API, модульне ядро й compiler-informed Virtual DOM.

Наступні релізи не ламали цю основу, а послідовно прибирали тертя:

  • Vue 3.1 дав реальний шлях міграції з Vue 2;
  • Vue 3.2 сформував сучасний SFC-синтаксис;
  • Vue 3.3 зробив TypeScript природною частиною компонентної моделі;
  • Vue 3.4 скоротив шаблонний код і зменшив зайві реактивні оновлення;
  • Vue 3.5 серйозно покращив SSR, watchers і повсякденну ергономіку;
  • Vue 3.6 готує альтернативний fine-grained renderer, не змінюючи знайомі базові API.

Тому різниця між раннім Vue 3.0 і сучасним Vue — не стільки в новій парадигмі, скільки в рівні зрілості.

У 2020 році Vue 3 дав потужний фундамент. До 3.5 він став значно зручнішим і надійнішим production-інструментом, а 3.6 перевіряє, наскільки далеко цю саму модель можна розвинути без переходу до Vue 4.

Офіційні джерела