Vue.js 91 🟢 Enabling Vapor Mode (vapor Keyword and Compiler Flags)
Vapor Mode is not enabled by a single global flag. It is an opt-in compilation target that is selected per component via the vapor attribute on <script setup>, and the application-level entry point determines whether the VDOM runtime is included in the bundle. A component with the attribute compiles to direct DOM operations; a component without it compiles to VNodes. The application created with createVaporApp contains only the Vapor runtime, while an application created with createApp and the interop plugin contains both runtimes and can mix the two component types.
Key point: The vapor attribute is the per-component opt-in. createVaporApp is the application-level choice that excludes the VDOM runtime. vaporInteropPlugin is the bridge that allows Vapor components inside a VDOM application, at the cost of keeping both runtimes in the bundle.
Why enabling Vapor is a per-component decision
The migration problem. Vapor Mode does not support the full Vue API surface. Options API, app.config.globalProperties, getCurrentInstance(), and @vue:xxx per-element lifecycle events are not available in Vapor components . A global switch that converted every component would break any component that relies on those features. The per-component attribute allows a codebase to adopt Vapor incrementally, starting with the components that are performance-sensitive and do not depend on the unsupported APIs.
The bundle problem. A pure Vapor application created with createVaporApp excludes the VDOM runtime entirely, which is where the significant bundle size reduction comes from . A mixed application created with createApp and vaporInteropPlugin includes both runtimes. The interop plugin is what makes Vapor components usable inside a VDOM tree, but it also negates the bundle-size benefit . The choice between the two entry points is the choice between compatibility and size.
The compiler problem. The vapor attribute is processed by @vitejs/plugin-vue. The plugin detects the attribute and routes the component to the Vapor compiler instead of the standard compiler . In a Nuxt application, the equivalent is vue: { vapor: true } in nuxt.config.ts, which enables the compiler support for the attribute across the application . Without the compiler configuration, the attribute has no effect.
a. The vapor Attribute and Component Compilation
The attribute is added to <script setup> in the single-file component. It only works with <script setup>; the Options API is not supported in Vapor Mode .
<!-- Vapor component -->
<script setup vapor>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
When the compiler sees the attribute, it emits code that creates the DOM structure once and binds renderEffect calls to the dynamic parts. The conceptual output for a component with a dynamic id attribute and a text interpolation is two effects: one for the attribute and one for the text node. When either value changes, only the corresponding effect re-runs; the component function does not re-execute .
A component without the attribute compiles to the standard VNode-producing render function. The two component types can coexist in the same application when the interop plugin is installed.
b. Application Entry Points
The application entry point determines which runtime is included and which component types are supported.
Pure Vapor application. createVaporApp creates an application that does not include the VDOM runtime. Vapor components work directly. VDOM components do not work unless the interop plugin is also installed, which defeats the bundle-size purpose .
import { createVaporApp } from 'vue'
import App from './App.vue'
createVaporApp(App).mount('#app')
Mixed application. createApp with vaporInteropPlugin creates a standard VDOM application that can render Vapor components. The VDOM runtime remains in the bundle, so the size reduction is not realized, but the compatibility is maximal .
import { createApp, vaporInteropPlugin } from 'vue'
import App from './App.vue'
createApp(App)
.use(vaporInteropPlugin)
.mount('#app')
The Vue team recommends placing the boundary between Vapor and VDOM components above repeated regions rather than inside them. A VDOM component inside a Vapor v-for crosses the boundary once per iteration, which multiplies the interop overhead. A single VDOM wrapper around the list crosses once .
c. Compiler Configuration and Build Flags
The vapor attribute requires compiler support. In a Vite project, the @vitejs/plugin-vue plugin handles the attribute automatically when the Vue version supports Vapor . Some community guides mention adding @vue/compiler-vapor as a dev dependency or setting template.compilerOptions.mode: 'vapor' in the Vite config, but the official release notes describe the attribute as the opt-in mechanism, not a compiler flag .
The compile-time flags documented by Vue (__VUE_OPTIONS_API__, __VUE_PROD_DEVTOOLS__, __VUE_PROD_HYDRATION_MISMATCH_DETAILS__) are not Vapor-specific. They control tree-shaking of features in the esm-bundler build and are configured through Vite’s define option . They do not enable or disable Vapor Mode. The vapor attribute and the application entry point are the only mechanisms that select the compilation target.
In Nuxt, the configuration is vue: { vapor: true } in nuxt.config.ts, which enables Vapor component support in the Nuxt build pipeline . SSR hydration for Vapor was not available in the alpha releases, but the Nuxt demo indicates that it is feature-complete in the 3.6 RC .
Complete Example Session
// ============================================
// PART 1: VAPOR COMPONENT
// ============================================
// VaporCounter.vue
<script setup vapor>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">Count: {{ count }}</button>
</template>
// ============================================
// PART 2: VDOM COMPONENT
// ============================================
// VdomCounter.vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">Count: {{ count }}</button>
</template>
// ============================================
// PART 3: PURE VAPOR APP
// ============================================
// main-vapor.js
import { createVaporApp } from 'vue'
import VaporCounter from './VaporCounter.vue'
createVaporApp(VaporCounter).mount('#app')
// No VDOM runtime; VdomCounter would not work here
// ============================================
// PART 4: MIXED APP
// ============================================
// main-mixed.js
import { createApp, vaporInteropPlugin } from 'vue'
import App from './App.vue'
createApp(App)
.use(vaporInteropPlugin)
.mount('#app')
// Both Vapor and VDOM components work
// ============================================
// PART 5: APP WITH BOTH
// ============================================
// App.vue
<script setup>
import VaporCounter from './VaporCounter.vue'
import VdomCounter from './VdomCounter.vue'
</script>
<template>
<div>
<VaporCounter />
<VdomCounter />
</div>
</template>
// ============================================
// PART 6: BOUNDARY PLACEMENT
// ============================================
// ❌ BAD: VDOM component inside Vapor loop
// <div v-for="item in items">
// <VdomCheckbox :item="item" />
// </div>
// ✅ GOOD: VDOM wrapper around the list
// <VdomList :items="items" />
// ============================================
// PART 7: NUXT CONFIGURATION
// ============================================
// nuxt.config.ts
export default defineNuxtConfig({
vue: { vapor: true }
})
// ============================================
// PART 8: UNSUPPORTED FEATURES
// ============================================
// Options API
// app.config.globalProperties
// getCurrentInstance() returns null
// @vue:xxx per-element lifecycle
The eight parts trace the component attribute, the two application entry points, the mixed application, and the Nuxt configuration.
Quick Reference
Enabling Vapor
| Mechanism | Where |
|---|---|
vapor attribute | <script setup vapor> |
| Pure app | createVaporApp(App) |
| Mixed app | createApp(App).use(vaporInteropPlugin) |
| Nuxt | vue: { vapor: true } in nuxt.config.ts |
Compile-Time Flags (Not Vapor-Specific)
| Flag | Purpose |
|---|---|
__VUE_OPTIONS_API__ | Tree-shake Options API |
__VUE_PROD_DEVTOOLS__ | Enable devtools in production |
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ | Detailed hydration warnings |
Supported vs. Unsupported
| Supported | Unsupported |
|---|---|
<script setup> | Options API |
| Composition API | globalProperties |
| Props, events, slots | getCurrentInstance() |
| Custom directives (new interface) | @vue:xxx lifecycle |
Best Practices
✅ Do This:
<!-- Add vapor only to components that do not need unsupported APIs -->
<script setup vapor> <!-- ✅ -->
// Use createVaporApp for new, self-contained apps
createVaporApp(App).mount('#app') // ✅
// Use vaporInteropPlugin when mixing in an existing VDOM app
createApp(App).use(vaporInteropPlugin).mount('#app') // ✅
❌ Don’t Do This:
<!-- Use vapor on a component that relies on getCurrentInstance() -->
<script setup vapor> <!-- ❌ -->
// Expect createVaporApp to render VDOM components without interop
createVaporApp(VdomComponent) // fails // ❌
// Nest VDOM components inside a Vapor v-for
<VdomComp v-for="i in items" /> // N interop crossings // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Vapor attribute has no effect | Compiler not configured | Update Vue to 3.6+, check plugin version |
| VDOM component fails in Vapor app | Interop plugin not installed | Install vaporInteropPlugin |
| Bundle size not reduced | Interop plugin pulls VDOM runtime | Use createVaporApp for pure apps |
getCurrentInstance() is null | Vapor does not expose instance proxy | Use alternative detection |
| Nuxt build fails | Vapor support not enabled | vue: { vapor: true } |
Real-World Examples
1. Vapor Component
<script setup vapor>
const count = ref(0)
</script>
2. Pure Vapor App
createVaporApp(App).mount('#app')
3. Mixed App
createApp(App).use(vaporInteropPlugin).mount('#app')
4. Nuxt Config
export default defineNuxtConfig({ vue: { vapor: true } })
5. Boundary Above Loop
<VdomList :items="items" />
6. Check Instance
getCurrentInstance() // null in Vapor
7. Slot Rendering
<template><slot /></template>
8. Event Delegation
<button @[event]="onClick" />
9. Bundle Size Check
vite build --mode production
10. Opt-Out
<script setup> <!-- no vapor attribute -->
Visual
The Opt-In Paths
┌──────────────────────────────────────────────────────────────┐
│ COMPONENT │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ <script setup vapor> → Vapor compiler ││
│ │ <script setup> → VDOM compiler ││
│ └─────────────────────────────────────────────────────────┘│
│ │
│ APPLICATION ENTRY │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ createVaporApp → Vapor runtime only ││
│ │ createApp → VDOM runtime only ││
│ │ createApp + interop → Both runtimes ││
│ └─────────────────────────────────────────────────────────┘│
│ │
│ The component attribute selects the compiler. │
│ The entry point selects the runtime. │
│ │
└──────────────────────────────────────────────────────────────┘
Pure vs. Mixed Bundle
┌──────────────────────────────────────────────────────────────┐
│ PURE VAPOR APP MIXED APP │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Vapor runtime │ │ VDOM runtime │ │
│ │ Vapor compiler │ │ Vapor runtime │ │
│ │ │ │ Interop plugin │ │
│ └────────┬────────┘ └────────┬────────┘ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ ~7.9 kB │ │ ~22.8 kB │ │
│ │ (Hello World) │ │ (Hello World) │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ The interop plugin keeps the VDOM runtime in the bundle. │
│ │
└──────────────────────────────────────────────────────────────┘
Interop Boundary
┌──────────────────────────────────────────────────────────────┐
│ VAPOR APP │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Vapor component ││
│ │ ││
│ │ ┌───────────────────────────────────────────────────┐ ││
│ │ │ VDOM wrapper (one crossing) │ ││
│ │ │ ┌─────────────────────────────────────────────┐ │ ││
│ │ │ │ VDOM list with 200 items │ │ ││
│ │ │ └─────────────────────────────────────────────┘ │ ││
│ │ └───────────────────────────────────────────────────┘ ││
│ └─────────────────────────────────────────────────────────┘│
│ │
│ One crossing, not 200. │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Component opt-in | <script setup vapor> |
| Pure app entry | createVaporApp(App) |
| Mixed app entry | createApp(App).use(vaporInteropPlugin) |
| Nuxt config | vue: { vapor: true } |
| VDOM runtime in pure app | Excluded |
| VDOM runtime in mixed app | Included |
| Options API | Not supported in Vapor |
getCurrentInstance() | Returns null in Vapor |
| Recommended adoption | Partial, performance-sensitive components |
Key takeaways:
- The
vaporattribute is the per-component opt-in. It only works with<script setup>and selects the Vapor compiler for that component. Without the attribute, the component compiles to VNodes as usual. The attribute can be applied to some components and not others in the same codebase . - The application entry point determines the runtime.
createVaporAppcreates an application without the VDOM runtime, which is where the bundle-size reduction comes from.createAppwithvaporInteropPluginincludes both runtimes and allows Vapor components to be rendered inside a VDOM tree . - The interop boundary has a cost. Every VDOM component instantiated from a Vapor template is a crossing. The Vue team recommends placing the boundary above repeated regions, not inside them, to avoid multiplying the crossings .
- The compile-time flags are not Vapor switches. The
__VUE_*flags control tree-shaking of features in theesm-bundlerbuild. They do not enable or disable Vapor Mode. Thevaporattribute and the entry point are the only mechanisms that select the compilation target . - Vapor Mode is not recommended for migrating existing components during the alpha and RC phases. The Vue team recommends partial usage in existing apps for performance-sensitive pages, or building small new apps entirely in Vapor .
Remember: Enabling Vapor Mode is a two-part decision. The component attribute selects the compiler for that component. The application entry point selects the runtime that is included in the bundle. A pure Vapor app uses both consistently and gets the size reduction. A mixed app uses the interop plugin and trades the size reduction for the ability to render both component types. The vapor attribute alone is not enough to reduce the bundle; the entry point must also exclude the VDOM runtime.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!