12k
All articles

Что такое Vue Vapor Mode?

Vue Vapor Mode: как SFC компилируются в прямые DOM-операции, что меняется в Vue 3.6 RC и какие ловушки есть с событиями и слотами.

OpenReplay Team
OpenReplay Team
Что такое Vue Vapor Mode?

Vue Vapor Mode — это режим компиляции однофайловых компонентов Vue, который превращает шаблоны в прямые операции с DOM, поэтому рендеринг и обновления происходят без создания и сравнения виртуального DOM. Это сокращает базовый размер бандла, стоимость обновлений и потребление памяти.

Vapor уже некоторое время показывают на конференциях, но в документации Vue страницы о нём до сих пор нет. Подробности живут в release notes ядра Vue.

В этой статье разбирается, что Vapor Mode меняет в работе ваших компонентов и на каком этапе релизного цикла он находится. Также рассматриваются два поведения Vapor, которые отказывают молча и в которые легко угодить.

Ключевые выводы

  • Vapor Mode компилирует Vue SFC в прямые операции с DOM вместо создания и сравнения VNode — именно отсюда берутся меньший бандл и более быстрые обновления.
  • Vapor Mode функционально завершён в release candidate-сборках Vue 3.6, но не является стабильным: ветка 3.6 всё ещё в предрелизном состоянии, версия v3.6.0-rc.9 (18 сентября 2026) помечена на GitHub как Pre-release, тогда как метка Latest остаётся на ветке 3.5 у версии v3.5.43 (17 сентября 2026).
  • Vapor включается явно и покомпонентно через <script setup vapor>, <script vapor> или <template vapor>, и работает только с SFC, состоящими только из шаблона, и с SFC, использующими script setup; Options API не поддерживается.
  • createVaporApp() монтирует чистое Vapor-приложение, которое никогда не загружает рантайм виртуального DOM; установка vaporInteropPlugin для размещения VDOM-компонентов возвращает этот рантайм обратно и нивелирует выигрыш в размере.
  • У Vapor-компонентов нет VNode и нет публичного прокси экземпляра, поэтому getCurrentInstance() возвращает null, app.config.globalProperties не применяется, а template ref на компонент больше не даёт доступа к $el, $props, $attrs, $slots или $refs.

Как работает Vue Vapor Mode?

Release note к v3.6.0-rc.1 представляет Vapor как второй способ компиляции однофайловых компонентов, нацеленный на меньший стартовый бандл и лучшую производительность. Ничего из этого не включается по умолчанию: вы активируете режим сами, по одному компоненту за раз, а та часть Vue API, которую он покрывает, в основном ведёт себя так, как вы и ожидаете. Исходный код, который вы пишете, не меняется. Меняется результат компиляции: вместо render-функции, порождающей VNode для последующего сравнения рантаймом с предыдущим деревом, компилятор генерирует код, который создаёт узлы один раз и связывает каждую реактивную зависимость с конкретным обновлением DOM, которым она управляет.

Удаление виртуального DOM убирает сразу три источника затрат. Сам рантайм диффинга в чистом Vapor-приложении вообще не попадает в сборку, поэтому базовый бандл меньше. Обновления пропускают выделение VNode и сравнение деревьев, поэтому изменение одного ref затрагивает один текстовый узел или один атрибут. А поскольку между рендерами не хранится теневое дерево, потребление памяти на компонент падает.

Сам компонент выглядит как обычный код Composition API:

<script setup vapor>
import { ref, computed } from 'vue'

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

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

От того же компонента, скомпилированного в режиме VDOM, отличается только атрибут vapor.

Статус релиза: функционально завершён, но не стабилен

Vapor Mode не выходил ни в одном стабильном релизе Vue. В списке релизов vuejs/core ветка 3.6 всё ещё находится в стадии release candidate: у версии v3.6.0-rc.9, опубликованной 18 сентября 2026 года, стоит метка Pre-release, тогда как метка Latest принадлежит v3.5.43, опубликованной 17 сентября 2026 года. npm подтверждает то же самое: dist-tag latest у пакета vue указывает на 3.5.43, а release candidate-сборки лежат за отдельным тегом rc. Стабильного тега 3.6.0 не существует.

То, что действительно верно, гораздо уже и легко путается с «уже вышло». Release note к v3.6.0-rc.1 сообщает, что Vapor Mode функционально завершён в Vue 3.6 RC — именно поэтому ветка 3.6 вообще перешла в стадию release candidate. Функциональная завершённость описывает объём возможностей, а не стабильность. Собственная релизная политика Vue трактует любой предрелиз одинаково: он нестабилен, существует для того, чтобы команды могли проверить, как сборка ложится на их стек, а не запускать её в продакшене, и вправе ломать совместимость от сборки к сборке. Если вы такую версию ставите, фиксируйте точный номер.

Как включить Vapor Mode?

Vapor включается для каждого компонента, а не для проекта целиком. Подходят два типа компонентов: однофайловый компонент, содержащий только шаблон, и компонент, написанный с script setup. Компоненты на Options API скомпилировать в Vapor нельзя вообще. Есть три способа пометить подходящий компонент: полная форма <script setup vapor>, её сокращение <script vapor> и маркер vapor на теге шаблона, который компилирует весь файл как Vapor.

<!-- Form 1: the explicit form -->
<script setup vapor>
  // ...
</script>

<!-- Form 2: shorthand for <script setup vapor> -->
<script vapor>
  // ...
</script>

<!-- Form 3: marks the whole SFC as Vapor -->
<template vapor>
  <!-- ... -->
</template>

Практическое следствие — необходимость аудита. Любой компонент, всё ещё написанный с data, methods или mounted, придётся перевести на script setup, прежде чем флаг vapor для него что-то изменит.

Можно ли смешивать Vapor- и VDOM-компоненты?

Vapor- и VDOM-компоненты смешивать можно, и то, как вы монтируете приложение, определяет, что попадёт в бандл. Если все компоненты — Vapor, монтируйте через createVaporApp(): этот путь оставляет рантайм виртуального DOM за пределами сборки, откуда и берётся резкое падение базового размера. Приложение, смонтированное через createApp(), должно установить vaporInteropPlugin, прежде чем сможет отрендерить Vapor-потомка. Vapor-приложение может установить тот же плагин, чтобы размещать VDOM-потомков, но при этом рантайм возвращается и бо́льшая часть экономии в размере теряется, как поясняет release note к 3.6.0-rc.1.

// Pure Vapor: the VDOM runtime is never loaded
import { createVaporApp } from 'vue'
import App from './App.vue'
createVaporApp(App).mount('#app')

// Existing VDOM app hosting Vapor components
import { createApp, vaporInteropPlugin } from 'vue'
import App from './App.vue'
createApp(App).use(vaporInteropPlugin).mount('#app')

Компонент, написанный как render-функция или на JSX, — тоже VDOM-компонент, поэтому внутри Vapor-приложения ему нужен interop. Interop не даёт индульгенции. Вложение одного режима в другой корректно обрабатывает обычные props, события и слоты, но пока не все краевые случаи, и библиотека компонентов, построенная на виртуальном DOM, под Vapor всё ещё может вести себя неправильно. Совет команды Vue — закреплять за каждой областью приложения один режим рендеринга и сводить смешанное вложение к минимуму.

От чего отказываются Vapor-компоненты?

У Vapor-компонента нет VNode и нет публичного прокси экземпляра. Каждый пункт из списка неподдерживаемых возможностей в release note к rc.1 вытекает из этой одной границы, и у каждого пункта есть конкретное следствие:

Не поддерживается в VaporЧто именно ломается
Options APIКомпоненты, использующие data, methods или опции жизненного цикла, вообще не могут быть скомпилированы в Vapor.
app.config.globalPropertiesГлобальные свойства, внедрённые плагинами, внутри Vapor-компонентов отсутствуют; внедряйте нужное явно.
getCurrentInstance()Возвращает null, поэтому сторонний код, обращающийся к внутреннему экземпляру, внутри Vapor-компонентов ломается.
События жизненного цикла элемента @vue:xxxХуки уровня элемента исчезли; используйте template ref вместе с onMounted.
v-memoРучной механизм мемоизации недоступен и должен быть удалён.
$el, $props, $attrs, $slots, $refs у template ref на компонентПаттерны «родитель лезет в потомка» ломаются; переносите контракт на props и emits.

Release note также аккуратен в оценке того, насколько близко это соответствие. Vapor стремится вести себя как виртуальный DOM, но два рендерера устроены настолько по-разному, что небольшие расхождения в краевых случаях ожидаемы, и такое расхождение считается ломающим изменением только в том случае, если прежнее поведение было задокументировано.

Две ловушки, о которых стоит знать заранее

Делегированные события и stopPropagation()

В дизайне rc.1 события, которые можно делегировать, обрабатываются на уровне document. Элемент сохраняет свой обработчик, но к самому элементу ничего не привязывается. Всю работу делает единственный слушатель на документе: он проходит по маршруту события и вызывает каждый встреченный по пути обработчик. Если предок по дороге вверх вызовет stopPropagation(), событие никогда не дойдёт до document, и такой обработчик никогда не сработает. Три формы записи обходят делегирование и привязывают слушатель прямо к элементу: @[event]="onClick", v-bind="{ onClick }" и v-on="{ click: onClick }".

<script setup vapor>
const onClick = () => save()
</script>

<template>
  <!-- the ancestor stops propagation, so a delegated handler never fires -->
  <div @click.stop>
    <button @click="onClick">Save</button>
  </div>

  <!-- binds directly to the element instead -->
  <div @click.stop>
    <button v-on="{ click: onClick }">Save</button>
  </div>
</template>

Этот тип отказа беззвучен. Ничего не выбрасывается, ничего не логируется, и мониторинг ошибок рапортует о здоровой сессии, пока пользователь жмёт на контрол, который ничего не делает. Выявить это помогает session replay: можно отследить клик, за которым не последовало ни одной мутации DOM, — а это и есть сигнатура несработавшего обработчика. То же самое касается границ Vapor/VDOM, где пробелы interop обычно проявляются как странности рендеринга, а не как исключения.

Бо́льшая часть изменений по ветке RC приходится на гидратацию и слоты, и лишь пара правок касается того, как объединяются обработчики событий, — поэтому фиксируйте конкретный RC и читайте CHANGELOG ветки minor для той версии, которую устанавливаете, вместо того чтобы считать описание из rc.1 всё ещё актуальным.

slots.default() рендерит, а не сообщает

В Vapor вызов slots.default() — это не бесплатный взгляд на слот. Вызов выполняет код рендеринга слота, который может создавать Blocks и DOM-узлы, настраивать реактивные эффекты и, если страница гидратируется, забирать во владение DOM, уже присланный сервером. Поэтому привычная для VDOM практика вызывать слот, чтобы решить, показывать ли fallback, в Vapor имеет последствия.

<script setup vapor>
import { useSlots } from 'vue'
const slots = useSlots()
// Wrong: this call renders the slot rather than inspecting it
const showFallback = !slots.default?.()
</script>

Выражайте это решение в шаблоне и позвольте шаблону управлять рендерингом слота:

<template>
  <slot>Fallback</slot>
</template>

Кому стоит попробовать Vapor Mode уже сейчас

Release note называет на этом этапе два сценария: внедрение Vapor в часть уже существующего приложения — например, на одной странице, где важна скорость рендеринга, — и написание небольшого нового приложения на Vapor с нуля. Следствие стоит сформулировать прямо. Проект с политикой «только стабильные зависимости», экран, построенный на VDOM-библиотеке компонентов, и кодовая база, всё ещё сидящая на Options API, — плохие кандидаты для первого захода: первый вообще не может установить предрелиз, а два других находятся ровно там, где кусаются interop и список неподдерживаемых возможностей.

Планирование с оглядкой на Vapor Mode

Относитесь к Vapor Mode как к изменению компилятора, которое можно оценить уже сегодня на одном экране, а не как к миграции, которую нужно ставить в план. Зафиксируйте конкретный release candidate, переведите одну страницу с большими списками или тяжёлой анимацией на <script setup vapor> и проверьте список релизов vuejs/core, прежде чем что-либо планировать вокруг стабильной 3.6.

FAQ

Работает ли Vapor Mode с Nuxt?

Да, в экспериментальном виде и только на Vue 3.6 или новее. Nuxt оставляет корень приложения на виртуальном DOM и позволяет помечать отдельные компоненты или страницы как Vapor, так что внедрение идёт по частям. Полностью Vapor-приложение на Nuxt пока невозможно, template ref, указывающий на Vapor-компонент, не даст вам элемент, а если установленная версия Vue старее, Nuxt выдаст предупреждение и отключит опцию обратно.

Нужен ли Vapor Mode, чтобы получить улучшения производительности реактивности в Vue 3.6?

Нет. Ветка 3.6 перестраивает ядро реактивности Vue поверх alien-signals, и полученный выигрыш в скорости и потреблении памяти распространяется на каждое приложение на этой ветке релизов, ничего включать не нужно. Vapor Mode — это отдельное, покомпонентное изменение компиляции, поэтому обычное VDOM-приложение, работающее на 3.6, уже получает улучшения реактивности без включения Vapor где-либо.

Поддерживает ли Vapor Mode серверный рендеринг и гидратацию?

Гидратация SSR входит в набор возможностей Vapor в release candidate-сборках Vue 3.6 — ранние альфа-сборки выходили без неё. Корректность гидратации была одной из самых активно правившихся областей по всей предрелизной ветке, поэтому фиксируйте конкретный release candidate и тестируйте серверный вывод, гидратацию и восстановление после mismatch на реальной странице, а не рассчитывайте на паритет с VDOM-рендерингом.

Будет ли сторонняя библиотека компонентов работать внутри Vapor-компонента?

Проверьте это, прежде чем на неё закладываться. Компоненты, поставляемые как render-функции или JSX, остаются VDOM-компонентами и требуют interop, а код библиотеки, вызывающий getCurrentInstance или читающий globalProperties, внутри Vapor-компонента ничего не найдёт. Всё, что построено на provide и inject, а не на экземпляре компонента, обычно продолжает работать — именно поэтому Nuxt утверждает, что большинство его собственных composables и встроенных компонентов под Vapor не требуют изменений.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.