| |

Vue.js 91 🟢 Enabling Vapor Mode (vapor Keyword and Compiler Flags)

⏱️ Estimated read: 10 min | |

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

MechanismWhere
vapor attribute<script setup vapor>
Pure appcreateVaporApp(App)
Mixed appcreateApp(App).use(vaporInteropPlugin)
Nuxtvue: { vapor: true } in nuxt.config.ts

Compile-Time Flags (Not Vapor-Specific)

FlagPurpose
__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

SupportedUnsupported
<script setup>Options API
Composition APIglobalProperties
Props, events, slotsgetCurrentInstance()
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

PitfallWhy It HappensFix
Vapor attribute has no effectCompiler not configuredUpdate Vue to 3.6+, check plugin version
VDOM component fails in Vapor appInterop plugin not installedInstall vaporInteropPlugin
Bundle size not reducedInterop plugin pulls VDOM runtimeUse createVaporApp for pure apps
getCurrentInstance() is nullVapor does not expose instance proxyUse alternative detection
Nuxt build failsVapor support not enabledvue: { 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

ItemValue
Component opt-in<script setup vapor>
Pure app entrycreateVaporApp(App)
Mixed app entrycreateApp(App).use(vaporInteropPlugin)
Nuxt configvue: { vapor: true }
VDOM runtime in pure appExcluded
VDOM runtime in mixed appIncluded
Options APINot supported in Vapor
getCurrentInstance()Returns null in Vapor
Recommended adoptionPartial, performance-sensitive components

Key takeaways:

  • The vapor attribute 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. createVaporApp creates an application without the VDOM runtime, which is where the bundle-size reduction comes from. createApp with vaporInteropPlugin includes 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 the esm-bundler build. They do not enable or disable Vapor Mode. The vapor attribute 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.

Was this helpful?


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!