Back to BlogJavaScript

Vue 3.6 Vapor Mode Hits Feature-Complete: What the No-VDOM Path Means in Practice

Vue 3.6 beta declared Vapor Mode feature-complete with full VDOM parity, opt-in per component, and benchmark numbers in the same bracket as Solid.js and Svelte 5. Here's how the compiler switch works and when the performance gains are actually real.

VueVapor ModeVirtual DOMPerformanceVue 3.6
Vue 3.6 Vapor Mode Hits Feature-Complete: What the No-VDOM Path Means in Practice

Vue 3.6 Beta tagged v3.6.0-beta.1 last week with a notable annotation from the core team: Vapor Mode is feature-complete. Full parity with the standard virtual DOM rendering path, minus one carve-out for <Suspense>. If you have been tracking this effort since it was first teased as an experimental opt-in, that milestone is real.

Here is what changed, how the compilation pipeline is different, and the practical question of when you should actually reach for it.

What Vapor Mode Is Replacing

Vue has always compiled templates into render functions that produce VNode trees. On every re-render, those trees get diffed against the previous output. The diff determines what changed, and Vue patches the real DOM accordingly.

That diffing step is the overhead Vapor eliminates. Vapor Mode compiles your template into fine-grained reactive effects directly wired to specific DOM nodes. When a reactive value changes, the compiled effect that depends on it runs and updates exactly that node — no tree traversal, no diff, no VNode allocation per render.

This is why Vapor benchmark results land in the same bracket as Solid.js and Svelte 5. Both frameworks compile to direct DOM operations for the same architectural reason. The VNode abstraction is not a fundamental requirement of reactivity — it is a runtime convenience that Vapor trades for compile-time precision.

Opting In

The switch lives directly on your <script setup> tag. One attribute:

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

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

<template>
  <button @click="count++">
    Clicked {{ count }} times ({{ doubled }} total)
  </button>
</template>

That vapor attribute is the whole toggle. Your reactivity API is identical — same ref, computed, watch, watchEffect, composables. The compilation output is different; the authoring experience is not.

One hard constraint: Vapor Mode only works for Single File Components using <script setup>. Options API components and render functions are unaffected by this flag.

Partial Adoption Is the Actual Design

The architecture is explicitly built for incremental rollout. Vapor and VDOM components interoperate inside the same component tree. A Vapor child renders fine inside a VDOM parent, and a VDOM child renders fine inside a Vapor parent. Vue's runtime bridges them transparently.

For existing apps, this means you do not migrate everything at once. Profile first, find the components that re-render most frequently or have the largest templates, then drop the vapor attribute on those. The rest of the tree runs unchanged.

Good first targets:

  • **High-frequency re-renders**: Live feeds, real-time tables, counters, interval-driven UI — anything re-rendering on every tick benefits most from eliminating per-render VNode allocation.

  • **Large templates**: The bigger the template, the more VNodes created per render, the higher the diff cost. Vapor's savings scale directly with template size.

  • **New isolated features**: Building a new page or section within an existing VDOM app? Write it in Vapor from the start and skip the migration step entirely.

The Performance Numbers, Framed Correctly

The headline figure circulating in benchmarks is 97% faster than standard VDOM in specific test scenarios. That number is accurate in the conditions where it was measured, but it should calibrate your expectations, not set them.

The gains are proportional to render frequency and template complexity. A component that mounts once and never re-renders gets nothing meaningful from Vapor. A component that re-renders on every keypress and outputs a 200-row table is exactly where those benchmark numbers originate.

Profile before opting anything in. Vapor adds no value to components that were not already the bottleneck.

What Is Still Missing

One known gap before stable: <Suspense> support. Components that rely on <Suspense> for async boundary management cannot opt into Vapor in the current beta. The Vue team has marked this as the final remaining feature before a 3.6 stable release.

A second practical limit is third-party UI libraries. Most ship pre-compiled VDOM output. Vapor benefits require the Vapor compiler to run on the source — if you are importing a compiled npm package, you get its VDOM-compiled output until that library ships an explicit Vapor build. Popular component kits are not there yet.

Trying It Today

npm install vue@next
# or
pnpm add vue@next

No additional Vite plugin configuration needed. @vitejs/plugin-vue already handles the vapor attribute in current releases. Add the attribute, run the dev server, and confirm in Vue DevTools that the component is listed as rendering in Vapor mode.

Stable 3.6 does not have a firm date yet, but with feature parity declared and only Suspense remaining, the beta is close enough to test against seriously. If a component keeps showing up in your profiler, flipping one attribute is a low-risk first experiment — and now that the feature set is locked, whatever you learn in beta should carry directly to stable.