
Vue Vapor Mode's Real Breakthrough Is the Migration Boundary
~ 8 min read
Vue 3.6 reached release-candidate status on 18 July with the intended feature set for Vapor Mode complete. Four days later, a second release candidate followed. The obvious story is speed: Vue says its new compilation mode has reached the same territory as Solid and Svelte 5 in a third-party benchmark.
That is impressive, but it is not the most important part.
Vapor Mode matters because it is opt-in at the component level. A team can apply it to a performance-sensitive part of an existing Vue application without first rewriting the rest of the product. That makes it less like a new framework competing for a greenfield project and more like a new rendering engine that can be introduced behind a deliberate boundary.
The quality of that boundary will decide whether Vapor is a useful engineering tool or merely another benchmark result.
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.
The Migration Boundary Is the Feature
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.

This is a better migration story than asking whether an entire organisation should “move to Vapor”. Framework changes become expensive when they combine 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 Is Both an Escape Hatch and a Tax
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.
The Compatibility Gaps Are Architectural Clues
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. Vapor delegates eligible events to document, so an ancestor calling stopPropagation() can prevent a delegated handler from running. Dynamic event names and object forms of v-on use direct listeners instead. 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 number of hydration and interop fixes listed in the first release candidate also deserves attention. Release-candidate means the intended feature set is complete, not that every production workload has already exercised it.
A Sensible 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, not only 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.
The Best Version of Vapor Is Boring
The successful Vapor adoption will probably not look like a framework rewrite. It will look like an ordinary performance change: one problematic region identified, one boundary selected, one set of measurements improved and one rollback path retained.
Vue’s significant achievement is not simply making a benchmark bar shorter. It is making a different rendering strategy available without demanding that an established application become a different application overnight.
That makes the migration boundary the feature to watch. Keep it coarse, keep it explicit and make Vapor earn its place with measurements from the product that will actually run it.