Skip to content
Release notesAdvanced Vue

Vue 3.6 and Vapor Mode: where things stand

Ten release candidates in under eleven weeks, one breaking change along the way, and the first framework support landing this week. Here is the state of Vue 3.6 on 8 October, and a plan for your own app.

vuejs.school team8 Oct 20267 min readVue 3.5.43 stable · 3.6.0-rc.10

Vue 3.6 entered its release candidate phase on 18 July, when 3.6.0-rc.1 declared the intended feature set for Vapor Mode complete. Nine more RCs have followed; the latest, 3.6.0-rc.10, came out on 30 September. The stable release you should run in production is still Vue 3.5.43, published on 17 September.

Vue 3.6 brings two big changes: Vapor Mode, a new way to compile components that you opt into one component at a time, and a rewrite of the reactivity system that every component runs on. If you maintain a Vue app, the step to take now is testing the RC in a branch. The facts below come from the Vue core releases page, the rc.1 release notes and the changelog.

Vapor Mode: opt in one component at a time

Vapor Mode is a second way to compile single-file components. A normal component compiles to a render function that builds a virtual DOM tree, which Vue compares with the previous tree to work out what to change. The Vapor compiler skips that tree and emits direct DOM operations, tied to the reactive values they read.

The rc.1 notes describe it as 100% opt-in, supporting a subset of Vue's APIs with mostly identical behaviour, with the goals of a smaller baseline bundle and better performance. You opt in per component with <script setup vapor>, its shorthand <script vapor>, or <template vapor> for template-only files.

In an existing app, Vapor components need the interop plugin, which lets Vapor and virtual DOM components nest inside each other:

src/main.ts
import { createApp, vaporInteropPlugin } from 'vue'
import App from './App.vue'

createApp(App).use(vaporInteropPlugin).mount('#app')

An app written entirely in Vapor can start with createVaporApp(App) instead and leave the virtual DOM runtime out of the bundle. The notes recommend two uses for now: a performance-sensitive page inside an existing app, or a small new app built fully in Vapor.

What Vapor components cannot use, per the rc.1 notes:

  • the Options API
  • app.config.globalProperties
  • getCurrentInstance(), which returns null
  • per-element @vue: lifecycle events
  • v-memo
  • properties such as $el and $refs on a template ref to a component

Two more things work differently. Custom directives use a new function-based interface: a directive receives the element and a getter for its value instead of a binding object, so directives written for virtual DOM components need rewriting. And calling slots.default() in a script runs the slot's rendering, which can create DOM nodes and reactive effects, so in Vapor it is not a safe way to inspect a slot; leave slot rendering to the template.

The reactivity rewrite reaches every component

Vue 3.6 refactors @vue/reactivity on top of the alien-signals library, which the notes say significantly improves the performance and memory use of the reactivity system. It is not opt-in: every component in a 3.6 app runs on it, Vapor or not. That makes it the part most likely to surface a difference in an existing app.

What the ten release candidates changed

The changelog at rc.10 lists 266 entries for rc.2 to rc.10: 248 bug fixes, 17 performance improvements and one feature. Nearly all are filed under the Vapor compiler and runtime, and they cluster in the same places: hydration of server-rendered Vapor output, slots, KeepAlive, Transition, fallthrough attributes and the boundary between Vapor and virtual DOM components. The last two RCs were the largest, with 53 entries in rc.9 and 77 in rc.10.

One change was breaking: rc.2 made event delegation opt-in. Up to rc.1, Vapor attached eligible event listeners to document and dispatched from there. An ancestor calling stopPropagation() could therefore stop a child's handler from ever running, unlike normal Vue. Since rc.2, listeners attach to the element itself, as in virtual DOM mode, and delegation is a per-listener choice made with the .delegate modifier:

src/components/RowList.vue
<script setup vapor lang="ts">
defineProps<{ rows: { id: number; label: string }[] }>()
const emit = defineEmits<{ select: [id: number] }>()
</script>

<template>
  <ul>
    <li v-for="row in rows" :key="row.id">
      <button type="button" @click.delegate="emit('select', row.id)">{{ row.label }}</button>
    </li>
  </ul>
</template>

Here the buttons share one click listener on document instead of one listener each, and the stopPropagation() trade-off comes back for these handlers only. The modifier only exists in Vapor components and only applies to supported static events. The old compilerOptions.eventDelegation setting was removed; delete it if you set it while trying a beta or rc.1.

Other fixes bring Vapor closer to normal components. rc.9, for example, made a Vapor component's props shallow, so watching props is no longer deep, as in a virtual DOM component.

A few fixes reach beyond Vapor. rc.10 includes fixes in the shared reactivity and runtime code, such as notifying the rest of a batch when one subscriber throws, and keeping a local useModel value when the prop did not change. Those affect every component, so run your whole suite against the RC, not only the pages you convert.

Which tools support Vapor so far

Three pieces of tooling have announced support in their own release notes:

  • Nuxt 4.6, released on 5 October, supports Vapor in interop mode. You set vue: { vapor: true } in nuxt.config.ts, the app root stays on the virtual DOM, and pages or components opt in with the vapor attribute. It requires Vue ^3.6.0-rc.2 or newer, and the release post says routing, useAsyncData, layouts and most built-in components work without changes.
  • create-vue has offered "Vue 3.6 (Release Candidate)" as an experimental option since version 3.23 in July. It sets every core Vue package to the rc tag through your package manager's overrides, so nothing pulls in a second copy of 3.5.
  • Vue (Official), the editor extension, listed support for Vapor directives in language tools 3.3.8, published on 22 July.

Component libraries are the open question. The rc.1 notes say interop covers standard props, events and slots but "does not yet account for all possible edge cases", and give a virtual DOM component library used inside Vapor as their example of where rough edges may remain. Their general advice applies: keep library components and Vapor components in separate regions of the app, and avoid nesting one mode inside the other as much as possible.

What to run in production

The 3.5 line has kept moving. Three patches shipped during the RC phase: 3.5.41 on 5 August, 3.5.42 on 27 August and 3.5.43 on 17 September. They are bug fixes in hydration, Suspense, the SFC compiler and the shared runtime. 3.5.43, for example, fixes nested Suspense patches being dropped during hydration and keeps optional props optional when a runtime prop uses a generic PropType. Upgrading within 3.5 is a patch release with no API changes, so stay current:

npm install vue@3.5.43

Vue Router 5.4 arrived on 7 October with useScrollRestoration and onRouteRendered, plus a breaking change confined to its experimental API, which now normalises query values to arrays.

Test 3.6 in a branch

Leave your main branch on 3.5.43 and install the RC in a new one. From the project root:

git switch -c try-vue-3-6
npm pkg set overrides.vue='$vue'
npm install vue@3.6.0-rc.10 --save-exact

What the last two lines do:

  • npm pkg set adds "overrides": { "vue": "$vue" } to package.json. Pinia, Vue Router and @vitejs/plugin-vue declare the Vue versions they accept as ranges such as ^3.5.11, and npm does not count a release candidate as inside such a range, so without the override npm stops with an ERESOLVE error, on this install or the next one. $vue stands for the version in your own dependencies; the single quotes stop the shell from reading it as a variable. pnpm and Yarn have their own override settings.
  • --save-exact writes 3.6.0-rc.10 rather than ^3.6.0-rc.10, so a later install cannot move you to a newer RC until you choose to.

Then, in that branch:

  1. Test without Vapor. Run your full test suite and a manual pass of your main flows.
  2. Check the compatibility list. Search your code for getCurrentInstance, globalProperties, v-memo, @vue: events, custom directives, and code that calls slots.default() to inspect a slot. Components that use them cannot become Vapor components as written.
  3. Convert one region. Pick a page that is measurably slow, convert its components to <script setup vapor>, install vaporInteropPlugin, and measure again with a production build.
  4. Report what breaks. Release candidates exist to find edge cases before a stable release. A minimal reproduction in an issue is the most useful thing a user can contribute at this stage.

If you start a small new app this week and can live with release candidate software, building it entirely in Vapor is one of the two uses the notes recommend. npm create vue@latest with the RC option sets the project up on the RC, but the generated src/main.ts still calls createApp: switch it to createVaporApp and add vapor to each component. If you add Vue Router, its RouterView and RouterLink are render-function components, which the notes say need vaporInteropPlugin even in a Vapor app; that brings the virtual DOM runtime back.

Check it stuck3 questions from the Performance and Vapor Mode quiz.
Practice now
Was this helpful?

Keep reading