Back to blog
Tutorials

Vue Vapor Mode: Ditching the Virtual DOM in Vue 3.6

Kim BoenderKim Boender
April 5, 2026 6 min read
Vue Vapor Mode: Ditching the Virtual DOM in Vue 3.6

Vue Vapor Mode: Ditching the Virtual DOM in Vue 3.6

I've been following the Vapor Mode development since Evan You first floated the idea years ago, and now that Vue 3.6 has shipped with Vapor in beta, it feels like a genuinely good time to sit down and work through what it actually means for day-to-day Vue development. Not just what the benchmarks say, but how you use it, where it makes sense, and where you shouldn't bother.

Short answer: it's impressive, it's opt-in, and you can start experimenting today without rewriting your app.

What Is Vapor Mode, Exactly?

Vue's standard rendering pipeline works through a Virtual DOM (VDOM). Your component's <template> compiles to a render function that returns a tree of virtual nodes. On every update, Vue diffs the old and new trees and applies only the necessary DOM changes. It works great, and has worked great for years.

Vapor Mode throws that diffing step out. Instead of generating virtual nodes, the Vapor compiler analyzes your template at build time and emits direct, fine-grained DOM update calls. The result is essentially hand-optimized imperative DOM code, generated automatically from your declarative template.

The inspiration is obvious if you've spent any time with Solid.js. Solid proved years ago that you can have clean, reactive, component-based code without a VDOM. Vue Vapor takes that concept and applies it to the Vue ecosystem while keeping the Single File Component format you already know.

How Much Faster Is It?

The headline number is up to 12x faster when creating 1,000 rows in microbenchmarks. In practice, the gains you'll see depend heavily on your app's profile:

  • Heavy list rendering / data tables: biggest gains. No VDOM diffing over large node trees.
  • Highly interactive UIs with lots of fine-grained state changes: noticeable improvement.
  • Static or near-static content: minimal difference. The VDOM overhead here was already tiny.

The memory footprint also shrinks, fewer intermediate objects means less garbage collection pressure. For apps running in constrained environments (low-end mobile, embedded webviews), that matters.

The Opt-In Model: This Is Important

Vapor Mode is not a breaking change. You don't have to migrate your app. You enable it on a per-component basis, and Vapor components coexist with standard VDOM components in the same application.

The mechanism that makes this work is the vaporInteropPlugin. It handles event propagation, ref access, and parent/child relationships across the Vapor/VDOM boundary so you can mix them freely.

This design was deliberate. A big-bang migration would have been a non-starter for most real apps. The gradual, component-level opt-in means you can start with your performance bottlenecks, measure, and move on.

Using Vapor Mode: A Practical Walkthrough

Let's look at what the code actually looks like. First, you need Vue 3.6 and the Vapor interop plugin if you're mixing modes.

npm install vue@3.6

A Standard Component (No Changes Needed)

<!-- UserCard.vue, regular VDOM component, unchanged -->
<script setup lang="ts">
defineProps<{
  name: string
  role: string
}>()
</script>

<template>
  <div class="user-card">
    <h3>{{ name }}</h3>
    <p>{{ role }}</p>
  </div>
</template>

Opting a Component into Vapor Mode

The change is exactly one keyword: vapor in the <script setup> tag.

<!-- DataTable.vue, Vapor component -->
<script vapor setup lang="ts">
import { ref } from 'vue'

const rows = ref<Array<{ id: number; value: string }>>([])

function loadData() {
  rows.value = Array.from({ length: 1000 }, (_, i) => ({
    id: i,
    value: `Row ${i}`
  }))
}
</script>

<template>
  <div>
    <button @click="loadData">Load Rows</button>
    <ul>
      <li v-for="row in rows" :key="row.id">{{ row.value }}</li>
    </ul>
  </div>
</template>

The only difference in your source code is vapor in the opening script tag. Under the hood, the compiler generates entirely different output, direct DOM creation and reactive update calls instead of virtual nodes.

Setting Up the Interop Plugin

In your main.ts, register the plugin so Vapor and VDOM components can communicate:

// main.ts
import { createApp } from 'vue'
import { vaporInteropPlugin } from 'vue/vapor'
import App from './App.vue'

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

After that, you can nest Vapor components inside VDOM components and vice versa without any extra ceremony.

Before/After: What the Compiler Actually Emits

To make this concrete, here's a simplified comparison for a basic counter component.

Standard VDOM output (simplified):

// Render function creates virtual nodes on every update
function render() {
  return createVNode('div', null, [
    createVNode('button', { onClick: increment }, 'Click'),
    createVNode('span', null, count.value)
  ])
}

Vapor output (simplified):

// Setup runs once; only the span's text node updates reactively
const div = createElement('div')
const button = createElement('button')
button.addEventListener('click', increment)
const span = createElement('span')
renderEffect(() => {
  span.textContent = count.value
})
appendChild(div, button, span)

The VDOM version re-runs the render function and diffs the result on every update. The Vapor version sets up the DOM once and surgically updates only the text node. For a counter, the difference is negligible. For a table with 500 rows, it compounds fast.

What About the Composition API and Pinia?

Good news: Vapor components use the same Composition API you already know. ref, computed, watch, provide/inject, all work identically. The vapor keyword only changes how the template compiles, not how your script logic runs.

Pinia stores work exactly as before too. There's nothing special to configure. A Vapor component can call useMyStore() and react to store state changes just like any other component, because Pinia is built on Vue's reactivity system, which Vapor still uses entirely.

Where to Start in an Existing App

My approach would be to profile first. Use Vue DevTools or browser performance traces to identify components that are genuinely slow, usually large list renders, heavy tables, or dashboards with lots of reactive data. Those are your candidates for Vapor.

Don't reach for Vapor on a <Header> component that renders once and barely changes. The VDOM overhead there is already essentially zero, and you'd be adding complexity for no gain.

Start small, benchmark the before/after with realistic data volumes, and expand from there.

Deployment Consideration

One thing worth noting: Vapor components produce slightly different compiled output, so it's worth making sure your deployment pipeline is running the latest Vite version (Vite 6.x supports Vue 3.6 + Vapor out of the box). If you're deploying to Vercel, there's nothing extra to configure, Vite-based projects deploy cleanly, and the build output is just static files. Same story with Railway if you're running a Nuxt 3 setup with SSR.

Is Vapor Mode Production-Ready?

As of Vue 3.6, it's in beta. The API is stable enough to experiment with, but I'd be hesitant to bet a critical production system on it just yet. The Vue team has been clear that the behavior and interop details may still shift before a stable release.

For new projects or non-critical components: absolutely worth exploring. For existing production apps: maybe give it a few more months and a stable release.

Either way, this is the most architecturally significant change Vue has shipped in years. The fact that it's opt-in makes it low-risk to try. I'm genuinely excited to see what the ecosystem builds once Vapor stabilizes, especially around component libraries that can ship Vapor-compiled packages for maximum downstream performance.


Frequently Asked Questions

What is Vue Vapor Mode? +
Vue Vapor Mode is a new rendering approach in Vue 3.6 that compiles Single File Components directly to DOM operations, bypassing the Virtual DOM entirely. This results in faster initial renders and more efficient fine-grained updates, particularly for data-heavy UIs.
Do I have to rewrite my Vue app to use Vapor Mode? +
No. Vapor Mode is completely opt-in at the component level. You add the vapor keyword to individual script setup tags. Your existing components continue to work unchanged, and Vapor and standard VDOM components can coexist via the vaporInteropPlugin.
Does Vapor Mode work with Pinia and the Composition API? +
Yes. Vapor components use the same Composition API (ref, computed, watch, etc.) and Pinia stores work without any modification. The vapor keyword only affects template compilation, not the reactivity or logic layer.
How much faster is Vapor Mode in practice? +
Benchmarks show up to 12x faster creation of large lists in ideal conditions. Real-world gains depend on your app, heavy list-rendering and highly interactive components benefit most. Static or low-update-frequency components will see minimal improvement.
Is Vue Vapor Mode stable enough for production use? +
As of Vue 3.6, Vapor Mode is in beta. The API is largely stable and safe to experiment with, but the Vue team recommends waiting for a stable release before deploying it in critical production applications.

Try it yourself

JSON Formatter

Format, validate, and beautify JSON instantly

Open JSON Formatter