Epicmax
September 9, 2026·12 min read· by Pavel Mironov

Vue Vapor Mode Migration: A Decision Framework for Existing Vue 3 Apps

Vapor Mode is a per-page decision, not an app rewrite. A budget-first framework for CTOs deciding whether to adopt Vue's virtual-DOM-less renderer.

Vue.jsPerformanceMigrationVapor Mode
Vue Vapor Mode Migration: A Decision Framework for Existing Vue 3 Apps

Your engineers have seen the benchmarks. Vue's new Vapor Mode posts numbers in the same league as Solid and Svelte 5, and someone on the team is now asking whether you should "do Vapor." The honest answer for most existing Vue 3 apps is: maybe, on one page, after you've measured. Vapor Mode is opt-in per component, and the Vue team's own guidance is to start with a single performance-sensitive page — not to rewrite an app.

This is a page-level decision, not an app-level one. And the deciding factor is rarely the benchmark. It's the composition of your codebase: Vapor components can't use the Options API, so how much of your app still lives there matters more than any percentage on a chart. This guide gives you a way to answer the question with a budget instead of an opinion.

Vapor Mode ships in Vue 3.6, which as of September 2026 is still a release candidate (v3.6.0-rc.7). APIs and behaviour can change before stable. Treat everything below as guidance for planning and prototyping, not a green light for production rollouts, and revisit the week 3.6 goes stable.

TL;DR

  • Vue 3.6 helps everyone; Vapor helps opt-ins. The reactivity rewrite (based on alien-signals) improves performance and memory for all 3.6 apps automatically. Vapor Mode is a separate, opt-in compilation mode you turn on per component.
  • It's a per-page decision. The Vue team recommends one performance-sensitive page in an existing app, or a small new app built entirely in Vapor — with distinct regions and minimal mixed nesting.
  • The real constraint is the Options API. Vapor components require Composition API / <script setup>. Your Options API share is the migration budget, not the vapor flag.
  • Bundle-size wins are for pure Vapor apps only. In an existing app you use the interop plugin, which pulls the virtual-DOM runtime back in and offsets that benefit.
  • Framework: profile first → inventory your Options API share → draw a clean region boundary → estimate, convert, and re-measure.

What 3.6 changes for everyone vs. only for Vapor opt-ins

The most useful thing a tech lead can do early is separate two changes that arrive in the same release but affect you very differently.

For everyone: the reactivity rewrite. Vue 3.6 includes a major refactor of @vue/reactivity based on alien-signals, which the release notes describe as significantly improving the reactivity system's performance and memory usage. You get this by upgrading. No component changes, no opt-in, no new mental model — ref, computed, and watch behave as before, just faster and lighter. For many teams this is the entire practical payoff of 3.6, and it lands without any of the constraints below.

For opt-ins only: Vapor Mode. Vapor is described in the release notes as "a new compilation mode for Vue Single-File Components (SFCs) with the goal of reducing baseline bundle size and improving performance." Crucially, it is "100% opt-in and supports a subset of existing Vue APIs with mostly identical behavior." Instead of compiling your template into virtual-DOM render functions, the Vapor compiler generates code that creates and updates real DOM nodes directly, driven by the same reactivity system. That's where the Solid/Svelte-class numbers come from — and where the constraints start.

So before anyone budgets a Vapor migration: upgrade to 3.6 first, measure, and see whether the free reactivity gains already close your performance gap. Sometimes they do.

A Vue Vapor Mode migration is a page-level decision, not an app-level one

The Vue team is specific about where Vapor belongs. From the 3.6 release notes, they recommend it for:

  • Partial usage in existing apps, such as implementing a performance-sensitive page in Vapor Mode.
  • Building small new apps entirely in Vapor Mode.

And a structural rule that matters more than it first appears:

we recommend having distinct regions in an app where one rendering mode or the other is used, and avoiding mixed nesting as much as possible.

Read that as an architectural constraint, not a style tip. Vapor and virtual-DOM components can interoperate, but every boundary between them has a cost — in runtime code, in cognitive overhead, and in the edge cases (below) that only bite at the seams. A single Vapor route with a clean edge is cheap to reason about. A Vapor leaf component buried three levels deep inside a virtual-DOM tree, with slots crossing the boundary in both directions, is where you spend your debugging budget. Pick a region you can draw a box around: a heavy data grid, a dashboard, a canvas-adjacent view, a high-frequency-update page.

What a Vapor component cannot do

This is the list that actually decides your timeline. A Vapor component supports a subset of Vue's APIs. Per the 3.6 release notes, these are not available:

Not supported in VaporWhat it means for migration
Options APIComponents must be Composition API / <script setup>. Any data(), methods, computed, mixins-based component has to be converted first.
app.config.globalPropertiesGlobals injected onto every instance (a common Vue 2 → 3 holdover for $http, $filters, i18n helpers) aren't visible. Move them to explicit imports or provide/inject.
getCurrentInstance()Returns null in a Vapor component. Any library or utility reaching for the internal instance handle will break.
v-memoThe manual memoization escape hatch is gone — Vapor's direct DOM updates change the performance model, but code relying on v-memo needs another approach.
@vue:xxx per-element lifecycle eventsPer-element hooks like @vue:mounted on a DOM node aren't available.
Component template refs ($el, $props, $attrs, $slots, $refs)A ref to a Vapor child component does not expose these properties. Code that reaches into a child via ref to read $el or $refs must be reworked.

None of these are exotic in a real Vue 3 codebase that grew out of a Vue 2 migration. globalProperties and getCurrentInstance() in particular hide inside plugins and shared utilities you didn't write. That's why the inventory step in the framework below is not optional.

Behavioural gotchas at the boundary

Even for components that look convertible, a few behavioural differences change how they run. These are the ones most likely to produce a bug that passes code review and fails in production.

Event delegation and stopPropagation()

Vapor delegates events to the document root rather than attaching a listener to each element. The consequence, straight from the release notes: if any ancestor calls stopPropagation(), the event never reaches document, and the delegated handler will not run. If your Vapor region sits inside a virtual-DOM shell that stops propagation somewhere up the tree — a modal, a dropdown, a drag handler — clicks inside Vapor can silently stop working. Audit for stopPropagation() on the path between your app root and the Vapor boundary.

slots.default() is not a dry run

It's tempting to call slots.default() to "peek" at slot content — count children, check whether a slot is empty, branch on it. In Vapor that's not a side-effect-free inspection. Per the release notes, calling it executes the slot's rendering logic, which may create Blocks and DOM nodes, register reactive effects, and claim existing SSR DOM during hydration. Calling it twice, or calling it just to test, can double-render or corrupt hydration. Treat slot invocation as the real thing.

Custom directive signature

Custom directives in Vapor use a different interface. The signature is a plain function:

type VaporDirective = (
  node: Element | VaporComponentInstance,
  value?: () => any,
  argument?: string,
  modifiers?: DirectiveModifiers,
) => (() => void) | void

The value arrives as a getter (() => any) rather than a resolved binding object, and cleanup is the returned function. Any custom directive used inside a Vapor region needs a Vapor-shaped implementation — so count your custom directives (and the third-party ones you depend on) as part of the surface area.

The interop boundary — and why bundle-size wins are conditional

There are two ways to run Vapor, and the difference decides whether you get the headline bundle-size benefit.

Pure Vapor app — createVaporApp(). You mount the whole app in Vapor. Apps created this way avoid pulling in the virtual-DOM runtime code, which lets the baseline bundle size be drastically reduced. This is the configuration the "smaller bundle" story is about — and it's realistic mainly for small new apps, because everything in the tree must obey the Vapor subset.

Mixed app — createApp() + vaporInteropPlugin. You keep your existing virtual-DOM app and render Vapor components inside it (or vice versa) through the interop plugin. This is the realistic path for an existing app. But the interop plugin pulls in the virtual-DOM runtime and offsets the benefits of a smaller bundle. In other words: in an existing app, adopt Vapor for the runtime/rendering performance of a specific region, not for bundle size. If a stakeholder is selling the migration on "smaller bundle," and you're an existing app using interop, that argument doesn't hold — set the expectation early.

A four-step decision framework

Answer "should we do Vapor?" with a budget by working these in order. If any step comes back negative, you have your answer without touching the compiler.

1. Profile first

Establish that you have a rendering problem, on a specific page, that Vapor addresses. Upgrade to 3.6 and measure with the reactivity rewrite already in place — it may have closed the gap for free. Use real profiling (browser performance traces, your own metrics) on the actual slow view. If the bottleneck is network, an oversized payload, an N+1 query, or an unmemoized computation, Vapor won't fix it, and you'll have spent a migration on the wrong problem.

2. Inventory your Options API share

For the candidate region, count what's actually there: Options API components, uses of globalProperties, getCurrentInstance(), v-memo, component-ref access to $el/$refs, and custom directives. This is the migration budget. A region that's already <script setup> with local state is cheap. A region leaning on global plugins and Options components is expensive — and the cost is conversion + retesting, not the vapor flag. A code audit is a fast way to get this number if you don't already track Composition-vs-Options coverage. For the broader Options-to-Composition question, our guide on refactoring to the Composition API covers the trade-offs in depth.

3. Draw the region boundary

Pick a region you can enclose in a single box, honouring the Vue team's "distinct regions, avoid mixed nesting" rule. Prefer a whole route or a self-contained widget over a deep leaf component. Trace what crosses the boundary: slots, event propagation (watch for stopPropagation() above it), refs into children, injected globals. The fewer things cross, the cheaper and safer the migration — and the fewer interop edge cases you inherit.

4. Estimate, convert, and re-measure

Now you can put a number on it: N components to convert, M directives to reshape, K global dependencies to make explicit, plus retesting. Convert the region behind the interop plugin, then re-measure against your step-1 baseline on the same profile. Keep it: only if the measured gain justifies the spend and the ongoing cost of a mixed-rendering app. If not, you've bought a clean answer cheaply.

When to skip Vapor Mode — a checklist

Skip it, at least for now, if any of these are true:

  • You haven't profiled, or the bottleneck isn't rendering.
  • The upgrade to 3.6 (reactivity rewrite) already meets your performance target.
  • The candidate region is heavily Options API and you're not otherwise planning to migrate it.
  • The region depends on globalProperties, getCurrentInstance(), or child $el/$refs access that would be expensive to rework.
  • You can't draw a clean boundary — the region is deeply interleaved with virtual-DOM components.
  • Your motivation is bundle size, but you're an existing app that would use interop.
  • You need production stability today and can't absorb an RC-stage dependency.

If most of your app is still Options API, the higher-leverage move is usually a Vue 3 / Composition API migration first. That's the work that makes Vapor cheap later — and it pays off with or without Vapor.

Three shapes this decision takes

Concretely, the decision usually resolves along codebase composition, not benchmark envy:

  • A modern <script setup> app with one heavy page — a large virtual table, a live dashboard, a data-dense editor. This is the ideal candidate: profile the page, convert just that region behind the interop plugin, re-measure. Small blast radius, clear boundary, measurable payoff.
  • A Vue 2 → 3 migration that leaned on globalProperties and still has Options components. Here the Vapor cost is dominated by prerequisite conversion. The right sequence is Composition API first, Vapor later (if ever). Don't let Vapor jump the queue ahead of the migration that unblocks it.
  • A brand-new, small internal tool. This is the one case where a pure Vapor app with createVaporApp() is worth it — you get the runtime performance and the genuine bundle-size reduction because there's no virtual-DOM runtime to pull in. Green-field, no Options API legacy, no interop tax.

The pattern across all three: the flag is trivial, the codebase decides. Measure the region, price the conversion, and the answer tends to write itself.

FAQ

Is Vue Vapor Mode production-ready?

Not yet as a general default. As of September 2026, Vapor Mode ships in Vue 3.6, which is still a release candidate (v3.6.0-rc.7, released 4 September 2026; the latest stable Vue is 3.5.42). The Vue team recommends scoping it to a single performance-sensitive page inside an existing app, or building a small new app entirely in Vapor Mode — not converting a whole production codebase. Treat it as a targeted optimization behind a measured decision, and re-evaluate once 3.6 reaches a stable release.

Does Vapor Mode support the Options API?

No. Vapor components are compiled from <script setup> / Composition API only. The Vue 3.6 release notes list the Options API among the unsupported features, alongside app.config.globalProperties, v-memo, and per-element @vue:xxx lifecycle events. getCurrentInstance() also returns null inside a Vapor component. This is why the real cost of adoption is usually your codebase's Options API share, not the vapor flag itself.

Do I need to rewrite my whole app to use Vapor Mode?

No — and you generally shouldn't. Vapor Mode is 100% opt-in and works per-component. You can keep your existing virtual-DOM app and render one region in Vapor via the interop plugin. A full-app rewrite is only worth considering for small new apps, where using createVaporApp() avoids pulling in the virtual-DOM runtime and reduces baseline bundle size. In an existing app, the interop plugin pulls that runtime back in, so the bundle-size win does not apply.

Deciding with a budget, not an opinion

Vapor Mode is a genuine step forward for Vue's performance ceiling, and the 3.6 reactivity rewrite is a free win for every app that upgrades. But "should we do Vapor?" is answered on your codebase, not on a benchmark. Profile the page, inventory the Options API share, draw a clean region, and re-measure. If the numbers justify it, convert one region and grow from there. If they don't, you've saved a migration.

If you want a second pair of eyes on that call, Epicmax has worked exclusively with Vue for over 8 years. We can profile the candidate page, quantify your Composition-vs-Options coverage, and tell you whether Vapor is worth it for your app — or whether a Vue 3 migration or a targeted code audit gets you further, faster.

Source: Vue 3.6.0-rc.1 release notes. Performance comparisons refer to the js-framework-benchmark project. Written September 2026 against Vue 3.6.0-rc.7; to be revisited when 3.6 reaches stable.

Vue.js is our passion. Open Source is our culture 😍