DEV Community

Cover image for Vue Vapor Mode: what it is and what changes in Vue 3.6
Thiago Nunes Batista for Código ao Ponto

Posted on Originally published at codigoaoponto.com AI-assisted

Vue Vapor Mode: what it is and what changes in Vue 3.6

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>
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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,
  },
})
Enter fullscreen mode Exit fullscreen mode

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 $el on 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 onPrehydrate falls back to the unkeyed form (and warns in development), because there is no component instance to attach the key to;
  • asyncData and fetchKey through defineNuxtComponent don't work (use useAsyncData in <script setup>);
  • Nuxt composables called after an await may 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, mounted and so on). If you want a refresher on the difference, see Composition API vs Options API;
  • app.config.globalProperties;
  • getCurrentInstance(), which returns null;
  • per-element lifecycle events such as @vue:mounted;
  • v-memo;
  • $el, $props, $attrs, $slots and $refs on 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" />
Enter fullscreen mode Exit fullscreen mode

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')
}
Enter fullscreen mode Exit fullscreen mode

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)