Vapor Mode in Practice

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

Reza Baar

August 5, 2026

What Actually Gets Faster (and What Doesn't Yet)

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.

What Vapor Mode Changes

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.

Opting a Component In

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.

Mixing Vapor and Standard Components

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.

Going Fully Vapor

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.

Where It Actually Helps

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.

Where It Doesn't Help (or Doesn't Work Yet)

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."

Where It Won't Help Much

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.

A Sensible Adoption Strategy

The approach that's emerged from early adopters is surgical, not wholesale:

  1. Identify one rendering-bound view in your app (a list, a dashboard, a data grid, wherever throughput is the visible bottleneck).
  2. Convert just that component to Vapor behind a feature flag.
  3. Instrument it and measure on real hardware with real data. A synthetic 100k-component demo tells you the ceiling, not your actual gain.
  4. Leave the rest of the app on the standard vdom path until the ecosystem and tooling solidify.

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."

Key Takeaways

  • Vapor Mode compiles components to direct DOM operations, skipping the virtual DOM
  • Opt in per component with <script setup vapor>, or go fully Vapor with createVaporApp
  • Mixed trees work via vaporInteropPlugin, though complex boundary cases need testing
  • The biggest wins are data-dense views, high-frequency updates, and bundle-constrained apps
  • Real limitations today: Composition API and <script setup> only, no Suspense, no per-element lifecycle hooks, new directive API, and vdom-oriented DevTools
  • It's feature-complete in the 3.6 beta but still labeled unstable, so adopt surgically behind feature flags
  • Some slowness is unstable props or missing virtualization, which Vapor doesn't fix

Conclusion

Vapor 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!

More certificates.dev articles

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.