
A hands-on look at opting individual components into Vue 3.6 Vapor Mode, where it helps, and the real limitations as of mid-2026.
Reza Baar
August 5, 2026
Vapor Mode is the headline feature of Vue 3.6. It's a new compilation strategy that skips the virtual DOM entirely for components that opt in, compiling templates directly to DOM operations. The benchmarks are striking (100,000 components mounting in around 100ms, on par with SolidJS). But as of mid-2026, it's feature-complete in the 3.6 beta while still labeled unstable, and it has real limitations. In this post, we'll opt individual components into Vapor Mode, look at where it actually helps, and be honest about what doesn't work yet.
Standard Vue compiles your templates into render functions that produce virtual DOM nodes. On every update, Vue creates a new vnode tree and diffs it against the previous one to figure out what changed. Vue 3 already made this fast with static hoisting and patch flags, but it still builds and diffs vnode trees at runtime.
Vapor Mode compiles the same template into code that touches real DOM nodes directly. There's no vnode tree, no diffing. When a reactive value changes, only the specific DOM node bound to it updates. This is the same fine-grained model that SolidJS and Svelte use.
The important thing for adoption: your code doesn't change. You write the same <script setup>, the same template syntax, the same composables. Only the compilation output differs.
You enable Vapor Mode per component with the vapor attribute on <script setup>:
<!-- components/DataRow.vue -->
<script setup vapor lang="ts">
defineProps<{
label: string;
value: number;
}>();
</script>
<template>
<div class="row">
<span>{{ label }}</span>
<span>{{ value }}</span>
</div>
</template>
That single vapor keyword is the entire opt-in. The component now compiles to direct DOM operations instead of a render function.
Real apps won't be all-Vapor overnight. Vue supports a mixed component tree where Vapor and standard vdom components coexist. To render Vapor components inside a standard app, you enable the interop plugin:
// main.ts
import { createApp } from 'vue';
import { vaporInteropPlugin } from 'vue';
import App from './App.vue';
const app = createApp(App);
app.use(vaporInteropPlugin);
app.mount('#app');
With this in place, a standard component can render a Vapor child and vice versa. Standard props, events, and slots work across the boundary. Complex cases (certain prop reactivity patterns, some slot edge cases) behave differently across the boundary and need testing, so don't assume every interaction is seamless.
If you're starting a new project or a bounded sub-app, you can skip the vdom runtime entirely with createVaporApp. This drops the virtual DOM runtime from the bundle:
// main.ts
import { createVaporApp } from 'vue';
import App from './App.vue';
createVaporApp(App).mount('#app');
When the whole app is Vapor, the base bundle drops under 10KB because you're not shipping the vdom runtime at all. This is the most dramatic bundle-size win, but it requires every component in the tree to be Vapor-compatible.
Here's the honest picture from benchmarks and early adopter reports. Vapor Mode delivers the most value in specific scenarios:
Data-dense views with frequent updates. Tables, dashboards, live feeds, anything rendering many rows that update often. This is the clearest win. A large data grid re-rendering on every filter change is exactly the workload where skipping vnode diffing shows up as smoother interaction.
Interactive tools where input latency matters. When every millisecond of input-to-paint latency is user-visible, the fine-grained updates help. Think editors, drawing tools, anything with tight interaction loops.
Bundle-size-constrained environments. If you're shipping to low-power devices or care a lot about the JavaScript payload, a fully Vapor app's sub-10KB base is meaningful.
Consider a data grid, the canonical case:
<!-- components/DataGrid.vue -->
<script setup vapor lang="ts">
defineProps<{
rows: Array<{ id: string; name: string; value: number; status: string }>;
}>();
</script>
<template>
<table>
<tbody>
<tr v-for="row in rows" :key="row.id">
<td>{{ row.name }}</td>
<td>{{ row.value }}</td>
<td>{{ row.status }}</td>
</tr>
</tbody>
</table>
</template>
With hundreds of rows updating on a data refresh, the standard version rebuilds and diffs a vnode tree for the whole table. The Vapor version updates only the cells whose values actually changed. That's where the smoothness comes from.
As of the 3.6 beta in mid-2026, here are the real constraints.
Composition API only. The Options API is not supported. If you're still using data(), methods, and the rest of the Options API, those components can't go Vapor without being rewritten to <script setup> first.
<script setup> only. It doesn't work with the manual setup() function form either. You need the compiler macro syntax.
Suspense is excluded. If your component tree relies on <Suspense> for async orchestration, those trees stay on the vdom path. This is a notable gap for apps built around async component loading.
Per-element lifecycle hooks don't work. Directives like @vue:mounted on individual elements aren't supported in Vapor components.
Custom directives use a new form. The old directive API doesn't carry over directly. Vapor uses a new form that takes a reactive getter and can return a cleanup function:
// Vapor-compatible custom directive
const highlight = (el: HTMLElement, valueGetter: () => string) => {
watchEffect(() => {
el.style.backgroundColor = valueGetter();
});
return () => {
// cleanup on unmount
};
};
A codemod is available to migrate existing directives, but it's still a migration step.
DevTools are optimized for vdom. Debugging Vapor components is less ergonomic right now because the tooling was built around the vnode model. Expect this to improve, but it's a real friction point today.
Ecosystem catch-up is incomplete. UI libraries work through vaporInteropPlugin, but complex components need testing. Community reports have flagged specific friction with certain integrations (Laravel Inertia page components were one example that surfaced in April 2026). If your UI library hasn't been tested against Vapor, budget time to verify.
It's still labeled unstable. Feature-complete in beta is not the same as production-default. Vue's own messaging is that beta means "ready for production evaluation," not "make it the default tomorrow."
Beyond the hard limitations, there are cases where Vapor just isn't the bottleneck. Content-heavy marketing sites with little interactivity won't see much, since the rendering cost isn't the constraint there. Apps with complex animation requirements may not benefit, and teams deeply invested in render functions or JSX patterns will find the transition rougher.
One thing worth remembering: many performance problems aren't about the vdom at all. Unstable props (creating new object literals on every render so children see "changed" props that didn't meaningfully change) are a common culprit that Vapor doesn't fix:
<!-- Avoid: new object every render -->
<Child :filters="{ status, ownerId }" />
<!-- Prefer: stable reference -->
<script setup lang="ts">
const filters = computed(() => ({ status: status.value, ownerId: ownerId.value }));
</script>
<template>
<Child :filters="filters" />
</template>
If your "slowness" is really update-cost from unstable props or missing virtualization on a long list, fixing those gives you more than switching to Vapor would.
The approach that's emerged from early adopters is surgical, not wholesale:
The migration cost is unusually low for a change of this magnitude. Composition API code is portable, the opt-in is per-file, and the interop story is real. Compared to something like the React class-to-hooks transition, this is one of the gentler paradigm shifts. But "gentle" doesn't mean "do it all at once."
<script setup vapor>, or go fully Vapor with createVaporAppvaporInteropPlugin, though complex boundary cases need testing<script setup> only, no Suspense, no per-element lifecycle hooks, new directive API, and vdom-oriented DevToolsVapor Mode is a genuine step forward, and the fact that it needs no API changes makes it one of the easiest major upgrades to pilot. But "feature-complete beta" comes with real constraints: no Options API, no Suspense, incomplete tooling, and an ecosystem still catching up. The right move today is to pick one rendering-bound view, ship it as Vapor behind a flag, and measure. By the time 3.6 goes stable, you'll already know whether it delivers for your specific app.
I hope this post has been helpful. Happy coding!
Get the latest news and updates on developer certifications. Content is updated regularly, so please make sure to bookmark this page or sign up to get the latest content directly in your inbox.

Vapor Mode in Practice
A hands-on look at opting individual components into Vue 3.6 Vapor Mode, where it helps, and the real limitations as of mid-2026.
Reza Baar
Aug 5, 2026

The First Hands-On AI Developer Certification
Discover how the AI Developer Certification measures real-world AI-assisted development through a hands-on, proctored exam. Learn what makes the certification unique, how it works, and what to expect when preorders open.
Alex Garrett-Smith
Jul 23, 2026

Nuxt Studio and What It Is
A look at Nuxt Studio, the open-source visual CMS for Nuxt Content, how it works with Git, and when it's the right fit.
Reza Baar
Jul 22, 2026