
Use Vue Vapor Mode at a Clear Migration Boundary
Revised
Published
~ 8 min read
Vue 3.6 reached release-candidate status on 18 July with the intended Vapor Mode feature set complete. The current pre-release is Vue 3.6.0-rc.6, published on 28 August 2026.
Vapor Mode is opt-in at the component level. Apply it to a measured bottleneck in an existing Vue application and leave unrelated routes on the current renderer. This gives the team a bounded performance test with a straightforward rollback.
The compiler changes the work done at runtime
Vue’s existing renderer builds virtual DOM nodes, compares one tree with another and applies the required changes to the browser DOM. The approach has served Vue well because it provides a flexible runtime model and supports features such as render functions and JSX.
Vapor Mode moves more of that work into the compiler. It analyses a Single-File Component and generates targeted DOM operations instead of creating and diffing virtual nodes for every update. Less generic machinery is needed in the browser, so there is an opportunity to reduce both runtime work and the baseline bundle.
The authoring model remains recognisably Vue:
<script setup vapor>
import { computed, ref } from "vue";
const quantity = ref(1);
const unitPrice = ref(24);
const total = computed(() => quantity.value * unitPrice.value);
</script>
<template>
<button @click="quantity++">Add one</button>
<output>£{{ total }}</output>
</template>
The vapor attribute changes how the component is compiled. It does not require a new template language or a different approach to reactivity.

There is a separate improvement in Vue 3.6 that is easy to conflate with Vapor. The release also includes a refactor of @vue/reactivity based on alien-signals. Vue’s maintainers report better performance and memory use from that work. It benefits the shared reactivity system; it is not evidence that an application has adopted Vapor Mode.
Keeping those changes separate matters when measuring an experiment. Compare a Vue 3.6 virtual DOM build with the equivalent Vue 3.6 Vapor build, otherwise the reactivity refactor can make Vapor appear responsible for gains it did not produce.
Choose a coarse migration boundary
The Vue 3.6 RC notes recommend two early uses: a small application built entirely in Vapor Mode, or partial adoption in an existing application, such as a performance-sensitive page.
The second case is the more interesting one.
Imagine a mature administration product with an established router, a virtual DOM component library and a data-heavy results page. The page renders thousands of cells, supports rapid filtering and is already known to be an interaction bottleneck. Replacing the entire application would be reckless. Rebuilding that one route as a self-contained Vapor region is a bounded experiment.
In an existing virtual DOM application, the interop plugin enables Vapor components:
import { createApp, vaporInteropPlugin } from "vue";
import App from "./App.vue";
createApp(App).use(vaporInteropPlugin).mount("#app");
The shell, navigation and unrelated routes can remain untouched. The team can measure the results page before and after: its route chunk, initial render, filter response, memory use and hydration behaviour. If the result disappoints, the rollback is also confined to that route.

Asking whether an entire organisation should “move to Vapor” combines a technical experiment with a product-wide rewrite. Component-level adoption lets a team prove the benefit where the cost is visible.
It also preserves one of Vue’s strengths: progressive adoption. Vapor does not need to win a philosophical argument about the one correct renderer. It needs to improve a real bottleneck without destabilising everything around it.
Interop has a cost
There is a catch. Vue supports virtual DOM and Vapor components nested inside one another, with standard props, events and slots generally covered. The maintainers nevertheless recommend keeping the rendering modes in distinct regions and avoiding mixed nesting.
That advice should be treated as an architectural constraint, not a temporary footnote.
If a pure Vapor application imports virtual DOM components, it must include the virtual DOM runtime. Render functions and JSX also remain virtual DOM features. The application may still gain faster updates in Vapor components, but part of the smaller-bundle argument has already been surrendered.
The same issue appears at a finer grain in an existing application. A Vapor results table whose every row contains several virtual DOM components will cross the interop boundary repeatedly. A heavily customised component library may make the proposed region Vapor in name but mixed in operation.
Before converting a component, inspect its dependencies:
- Does it render virtual DOM components, JSX or render functions?
- Does a design-system component expose behaviour through component instances?
- Do directives or slot utilities assume virtual DOM semantics?
- Can the boundary be moved upwards so that a whole route or feature uses one renderer?
Interop is valuable because it makes migration possible. It is a tax because every exception weakens the simplicity, bundle and debugging benefits that made the migration attractive.
Check compatibility before converting
Vapor Mode intentionally supports a subset of Vue. The Options API is not supported. Neither are app.config.globalProperties, v-memo or per-element @vue:xxx lifecycle events. getCurrentInstance() returns null, and component template refs do not expose familiar instance properties such as $el and $refs.
Those omissions reveal the sort of code most likely to migrate cleanly. Components that express their behaviour through Composition API state, explicit props, emitted events and templates are good candidates. Components that reach into public instances or depend on virtual DOM details need more scrutiny.
Some differences are subtler. Since Vue 3.6.0-rc.2, Vapor
attaches DOM listeners directly by default. Document-level delegation is opt-in for supported static events through the
Vapor-only .delegate modifier:
<button @click.delegate="onClick">Save</button>
This change aligned the default with standard Vue and native DOM event behaviour. Remove the earlier
compilerOptions.eventDelegation option if it remains in an RC configuration and use .delegate only where the
application has measured a benefit. Dynamic event names and object forms of v-on continue to use direct listeners.
Calling slots.default() is not a harmless inspection because it can render DOM, establish reactive effects and claim
server-rendered nodes during hydration. Custom directives also use a different interface.
These are not reasons to dismiss Vapor. A fundamentally different renderer cannot promise that every undocumented edge case behaves identically. They are reasons to test behaviour rather than assuming that successful compilation proves compatibility.
The Vue 3.6 changelog records many fixes through rc.6 for
hydration, virtual DOM interop, slots, KeepAlive, HMR and DOM properties. That is useful progress, but it also supports
a cautious pilot. Release-candidate means the intended feature set is complete, not that every production workload has
already exercised it.
Run a measured Vapor pilot
I would not begin by changing the application entry point to createVaporApp(). I would begin with one region whose performance problem is already understood.
First, capture a baseline on Vue 3.6 without Vapor. Record transferred JavaScript, the route’s own chunk size, relevant browser timings and memory under a repeatable workload. Framework benchmark rankings are no substitute for an application baseline.
Second, choose a coarse boundary. A route, embedded tool or isolated panel is easier to reason about than a scattered set of leaf components. Prefer a feature with limited dependence on a virtual DOM component library.
Third, inventory unsupported assumptions before editing. Search for Options API components, JSX, render functions, instance access, custom directives, manual slot invocation and event propagation tricks. The result may show that a different feature is a better first candidate.
Fourth, convert the region and test it as a user would use it. Include server rendering and hydration if the production path includes them. Exercise keyboard input, focus, transitions, slots and rapid updates as well as the happy-path render.
Finally, compare the result with the baseline and keep the decision reversible. A pilot succeeds when it produces trustworthy evidence, including evidence that the current renderer is already good enough.
Do not migrate for a benchmark
Vue links to js-framework-benchmark when describing Vapor’s performance. It is a useful independent project that measures repeatable operations such as creating, updating, swapping and removing rows in large tables. It is not a model of every Vue application.
A content site dominated by network requests and static rendering will not become meaningfully better because one renderer swaps table rows faster. An internal tool may be limited by an expensive charting library rather than virtual DOM work. A poorly designed state model can create unnecessary updates in either mode.
The benchmark establishes that Vue’s compiler can generate competitive low-level rendering code. It does not establish that migrating a particular component will improve a user-visible outcome.
That distinction is healthy. Vapor Mode does not need to justify a mass migration to be worthwhile. It only needs to give Vue teams a precise option when runtime rendering or baseline JavaScript is a measured constraint.
Keep the boundary coarse
Treat Vapor as an ordinary performance change. Identify one slow region, select a clear boundary, compare it with a Vue 3.6 baseline and retain a rollback path.
Adopt it only when measurements from the application show a useful improvement. A benchmark result on its own is not a reason to migrate.