Vue 3.6 is not stable yet. As of October 6, 2026, the latest version is 3.6.0-rc.10 (September 30), and the stable release is still 3.5.43 (September 17). I checked both on npm and in the vuejs/core releases on that date.
Vue Vapor Mode is the headline feature of 3.6: a compilation mode that skips the Virtual DOM and generates code that updates the DOM directly. It is opt-in, so you pick which components use it and you don't rewrite your app. This article covers how Vapor works, how to enable it in Vue and in Nuxt 4.6, what it doesn't support yet, and when it's worth a try.
How Vapor works
In the traditional mode, when reactive state changes, Vue runs the render function, builds a new tree of VNodes (plain JavaScript objects that describe the UI), compares it with the previous tree, and only then touches the real DOM.
In Vapor Mode, the compiler analyzes the template and generates code that manipulates the DOM directly. Vapor components don't create VNodes or diff trees at runtime. When a reactive value changes, only the part of the DOM bound to it is updated.
According to the official Vue 3.6 release notes, the goal is a smaller baseline bundle and better performance. The same notes say Vapor reached the same level of performance as Solid and Svelte 5 in third-party benchmarks (js-framework-benchmark). As with any benchmark, the result depends on the scenario measured.
What about Alien Signals?
Vue 3.6 also ships a major refactor of @vue/reactivity based on the alien-signals library. The rc.1 notes say it improves the reactivity system's performance and memory usage. It's an internal change: ref, reactive, computed and watch keep the same API. Since it's part of Vue 3.6, you get it whether or not you use Vapor.
How to enable Vapor Mode
Vapor works with SFCs that use <script setup> or only a template. You mark the component with the vapor attribute:
<script setup vapor lang="ts">
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">
count is {{ count }}
</button>
</template>
Outside Nuxt, @vitejs/plugin-vue compiles Vapor SFCs (the plugin's changelog records Vapor component support, including template-only ones), so use a recent version.
Vue also accepts <script vapor> as shorthand for <script setup vapor>. You can instead put the marker on the template with <template vapor> to compile the whole SFC in Vapor mode.
Vapor components in an existing app
In an app created with createApp(), you need to install vaporInteropPlugin to use Vapor components:
import { createApp, vaporInteropPlugin } from 'vue'
import App from './App.vue'
createApp(App).use(vaporInteropPlugin).mount('#app')
With the plugin installed, Vapor and regular components can be nested inside each other. The Vue notes warn that this covers props, events and slots in standard usage, but not every edge case yet. Their advice is to keep distinct regions in the app, each in one mode, and to avoid mixed nesting where you can.
A fully Vapor app
If every component is Vapor, use createVaporApp():
import { createVaporApp } from 'vue'
import App from './App.vue'
createVaporApp(App).mount('#app')
The Virtual DOM runtime then stays out of the bundle. Each component still needs the vapor marker. Components written with render functions or JSX remain Virtual DOM components and need interop. A Vapor app can also install vaporInteropPlugin to accept Virtual DOM components, but that pulls the runtime back in and cuts into the bundle savings.
Vapor in Nuxt 4.6
Nuxt 4.6, released on October 5, 2026, supports Vapor in interop mode. The app root stays on the Virtual DOM, and you choose which components or pages use Vapor. To enable it, turn the option on in nuxt.config.ts:
export default defineNuxtConfig({
vue: {
vapor: true,
},
})
Then mark components with <script setup vapor>, as in the example above. Nuxt requires Vue ^3.6.0-rc.2 or newer. According to the release post, routing, useAsyncData, layouts and most built-in components keep working unchanged. The post recommends trying Vapor in a fresh project first.
The Nuxt docs list a few limitations of interop mode:
- the app root can't be Vapor, because fully Vapor apps aren't available in Nuxt;
- Vapor components don't expose
$elon template refs; -
<ClientOnly>skips its attribute forwarding, and<NuxtIsland>, server components and the head components that read text children (<Title>,<Style>and<Noscript>) warn if you pass them a Vapor slot; - keyed
onPrehydratefalls back to the unkeyed form (and warns in development), because there is no component instance to attach the key to; -
asyncDataandfetchKeythroughdefineNuxtComponentdon't work (useuseAsyncDatain<script setup>); - Nuxt composables called after an
awaitmay lose their context.
The same release post has performance numbers for Nuxt 4.6: nuxt build on a starter project went from 4.4 s to 3.6 s (18% faster), and a page with 300 <NuxtLink>s went from 110 to 140 req/s in SSR (+26%). Nuxt credits those gains to general optimizations in the release, not to Vapor.
If you're still on Nuxt 3, it reached end of life on July 31, 2026.
What Vapor doesn't support yet
Vapor supports a subset of Vue's APIs. The official notes list what is unavailable or doesn't apply in Vapor components:
- the Options API (
data,methods,mountedand so on). If you want a refresher on the difference, see Composition API vs Options API; -
app.config.globalProperties; -
getCurrentInstance(), which returnsnull; - per-element lifecycle events such as
@vue:mounted; -
v-memo; -
$el,$props,$attrs,$slotsand$refson component template refs.
Component libraries built on the Virtual DOM also deserve testing. The notes mention "rough edges" when using a Virtual DOM component library inside Vapor, without naming any library. If one of them relies on getCurrentInstance(), the null above could be a problem, but only a test in your own project will tell.
The official notes don't list <Suspense> or Vue DevTools as incompatible with Vapor, but that doesn't guarantee full support. The RC changelog records several fixes for Vapor and Suspense integration, so support is still being worked on. Test both in your own setup before deciding.
The same goes for SSR. The RC changelog has several hydration fixes for Vapor components, which suggests SSR is supported but still being tuned. If your app renders on the server, test hydration carefully.
Events and .delegate
In earlier RCs, Vapor delegated events to document, so a stopPropagation() on an ancestor could stop a child's handler. Since rc.2, listeners are attached directly to the element, as in regular Vue, and delegation became opt-in through the .delegate modifier:
<button @click.delegate="onClick" />
The compilerOptions.eventDelegation option was removed.
Custom directives
Custom directives also have a different interface in Vapor. Instead of an object with hooks, you write a function that receives the element and a reactive getter, and it can return a cleanup function:
import { watchEffect } from 'vue'
const MyDirective = (el, source) => {
watchEffect(() => {
el.textContent = source()
})
return () => console.log('cleanup')
}
The full signature is (node, value?, argument?, modifiers?), where value is the reactive getter. For a refresher on the traditional model, see what directives are in Vue.
Where Vapor helps
The gain shows up where an app does a lot of DOM update work: dense lists and tables, components that change several times per second, and pages where initial JavaScript size matters. For an ordinary CRUD app, users will likely not notice the difference.
Vapor also doesn't fix problems that don't come from the renderer. Rendering 10,000 items in the DOM is slow in both modes, and the fix is to virtualize the list. Calling heavy functions straight from the template, like {{ calculateTotal(item) }}, is still a bad idea in both modes.
Should you adopt it now?
The Vue team says Vapor's feature set is complete in the RC and recommends two uses for now: applying Vapor to parts of an existing app, such as a performance-sensitive page, and building small new apps entirely in Vapor.
For a production app that already runs well on Vue 3.5, waiting for the stable release is the safer choice. For an isolated page where you've already measured a bottleneck, a trial in Nuxt 4.6 or in a Vue app with interop is a reasonable experiment. Before you start, check that:
- you measured current performance (the browser's Performance panel works) and found a rendering bottleneck;
- the component uses
<script setup>or only a template; - the UI libraries it relies on work with interop in your project;
- you can isolate Vapor to one route or component and back it out without pain.
Top comments (0)