How to Migrate a jQuery Codebase to Vue 3
How do I migrate a jQuery codebase to Vue 3?
TL;DR
- Bottom line: Migrate incrementally — mount Vue 3 apps into jQuery-managed pages using
createApp().mount(), replace jQuery patterns one widget at a time with reactive state and directives, then remove jQuery when no code depends on it. - Key tool/command:
createApp({ setup() { ... } }).mount('#vue-mount') - Watch out for: Direct DOM manipulation inside Vue components — mixing
$(el).html()with Vue's reactivity system causes the virtual DOM to desync from the real DOM. - Works with: Vue 3.3+ (3.5 recommended), any jQuery version (1.x–4.x), works with Vite, Webpack, or plain script tags (CDN).
Constraints
- Never mix jQuery DOM manipulation and Vue reactivity on the same DOM node — Vue's virtual DOM will overwrite jQuery changes on re-render.
- Vue 3 requires the full build (
vue.global.jsorvue.esm-browser.js) when using in-DOM templates during incremental migration — the runtime-only build excludes the template compiler. - Always destroy jQuery plugins in Vue's
unmounted/onUnmountedlifecycle hook to prevent memory leaks and ghost DOM elements. - Do not use
v-htmlto inject jQuery-rendered HTML — it bypasses Vue reactivity and creates XSS vulnerabilities. - jQuery 4.0 slim build removes Deferreds/Callbacks — if your codebase uses
$.Deferred, use the full jQuery 4.0 build or migrate to native Promises before switching to Vue composables. [src7]
Quick Reference
| jQuery Pattern | Vue 3 Equivalent | Example |
|---|---|---|
$('#el').html(val) | ref() + template interpolation | const text = ref('') → <p>{{ text }}</p> |
$('#el').on('click', fn) | v-on / @click directive | <button @click="handleClick"> |
$('#el').toggle() | v-if / v-show directive | <div v-show="isVisible"> |
$('#el').addClass('active') | :class binding | <div :class="{ active: isActive }"> |
$('#el').css({color: 'red'}) | :style binding | <div :style="{ color: textColor }"> |
$('#el').val() | v-model two-way binding | <input v-model="searchQuery" /> |
$.ajax({url, success}) | fetch + onMounted / composable | onMounted(async () => { data.value = await fetch(url).then(r => r.json()) }) |
$('#el').animate() | <Transition> / <TransitionGroup> | <Transition name="fade"><div v-if="show">...</div></Transition> |
$(document).ready(fn) | onMounted(() => {}) | Runs after component is inserted into DOM |
$.each(arr, fn) | v-for directive | <li v-for="item in items" :key="item.id">{{ item.name }}</li> |
$('#el').find('.child') | Template refs + useTemplateRef() | const el = useTemplateRef('container') (Vue 3.5+) [src6] |
$('#form').serialize() | v-model + reactive object | const form = reactive({ name: '', email: '' }) |
$('#el').show() / .hide() | v-show (CSS toggle) | <div v-show="isVisible"> (keeps DOM, toggles display) |
$.Deferred() | Native Promise / async/await | const data = await fetch(url).then(r => r.json()) |
$('#plugin').pluginName() | Custom directive or composable | app.directive('tooltip', { mounted(el) { ... } }) |
Decision Tree
START
├── Is the jQuery codebase < 500 lines with simple event handling?
│ ├── YES → Skip Vue, replace jQuery calls with vanilla JS (see jquery-to-vanilla-js)
│ └── NO ↓
├── Is this a full rewrite or incremental migration?
│ ├── FULL REWRITE → Scaffold new Vue 3 project (npm create vue@latest), rebuild from scratch
│ └── INCREMENTAL ↓
├── Does the page have isolated UI widgets (modals, tabs, forms)?
│ ├── YES → Start with "Vue Islands": mount Vue apps into existing page via createApp().mount()
│ └── NO ↓
├── Does the jQuery code heavily mutate shared global state (window.appState, data-* attrs)?
│ ├── YES → First extract state into a Pinia store or provide/inject, then migrate UI
│ └── NO ↓
├── Are there jQuery plugins with no Vue equivalent?
│ ├── YES → Wrap them in a custom directive using mounted/unmounted hooks (see Step 6)
│ └── NO ↓
├── Is the app server-rendered (PHP, Rails, Django templates)?
│ ├── YES → Use Vue's in-DOM template mode: createApp with existing HTML as template
│ └── NO ↓
└── DEFAULT → Migrate page-by-page: replace jQuery selectors with Vue components, use v-model for forms, replace $.ajax with fetch/composables
Decision Logic
Structured if/then rules for agent-driven recommendations. Each rule maps a concrete migration condition to an action.
If the jQuery codebase is under 500 lines with only simple event handlers
→ Skip Vue entirely; replace jQuery calls with vanilla JS (querySelector, addEventListener, fetch). [src3]
If you need a no-build-step incremental migration on a server-rendered page (PHP, Rails, Django)
→ Load Vue 3 via CDN (vue.global.prod.js) and mount widgets with createApp(...).mount('#id'); the full build is required because in-DOM templates need the compiler. [src1, src4]
If the page has isolated UI widgets (modals, tabs, forms) embedded in jQuery
→ Migrate as "Vue Islands": mount one createApp() per widget into its own container, leave the rest to jQuery, and remove jQuery only after every island is migrated. [src1, src4]
If jQuery code stores shared state in the DOM (data-* attrs, hidden inputs, classes)
→ Extract that implicit state into a Pinia store or a shared reactive() bridge object first, then migrate the UI on top of it. [src4]
If the app depends on a jQuery plugin with no Vue equivalent (date picker, rich-text editor, chart)
→ Wrap the plugin in a Vue custom directive, init in mounted, sync in updated, and always destroy in unmounted to prevent leaks. [src5, src8]
If you are starting a green-field rewrite and want maximum runtime performance
→ Scaffold with npm create vue@latest on Vue 3.5 (stable), and evaluate Vue 3.6 Vapor Mode (<script setup vapor>) only for new, self-contained components — it is feature-complete but still unstable as of mid-2026. [src6, src9]
If your codebase calls $.Deferred and you plan to use the jQuery 4.0 slim build
→ Convert Deferreds to native Promises before removing jQuery, since the 4.0 slim build drops Deferreds/Callbacks; otherwise stay on the full 4.0 build during migration. [src7]
Step-by-Step Guide
1. Audit the jQuery surface area
Grep your codebase to quantify the migration scope. Count AJAX calls, event bindings, DOM manipulations, and plugin usages. This converts a vague rewrite into discrete, estimable tasks. [src3]
# Count jQuery usage patterns across the codebase
grep -rn '\$\.' --include='*.js' --include='*.html' | wc -l
grep -rn '\$\.ajax\|\.get(\|\.post(' --include='*.js' | wc -l
grep -rn '\.on(\|\.click(\|\.submit(' --include='*.js' | wc -l
grep -rn '\.html(\|\.text(\|\.val(\|\.append(' --include='*.js' | wc -l
Verify: Review the counts — a typical medium app has 50–200 jQuery call sites. Prioritize pages by traffic and complexity.
2. Add Vue 3 alongside jQuery
Install Vue 3 into your existing project without removing jQuery. Both libraries can coexist because Vue only manages DOM inside its mount container. [src1]
# Option A: npm install (for bundler-based projects)
npm install vue@3
# Option B: CDN (for no-build-step projects)
# <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
Verify: import { createApp } from 'vue' compiles without errors, or window.Vue is available in the browser console.
3. Create mount points and mount Vue apps
Add container elements where Vue will take over. jQuery continues managing everything else. Vue 3 supports multiple independent app instances on one page. [src1, src4]
import { createApp, ref } from 'vue';
const SearchWidget = {
setup() {
const query = ref('');
const results = ref([]);
async function search() {
const res = await fetch(`/api/search?q=${encodeURIComponent(query.value)}`);
results.value = await res.json();
}
return { query, results, search };
},
template: `
<div>
<input v-model="query" @keyup.enter="search" placeholder="Search..." />
<button @click="search">Search</button>
<ul>
<li v-for="item in results" :key="item.id">{{ item.title }}</li>
</ul>
</div>
`
};
const container = document.getElementById('vue-search-widget');
if (container) {
createApp(SearchWidget).mount(container);
}
Verify: The Vue component renders within the existing page layout without breaking jQuery functionality.
4. Replace jQuery DOM manipulation with Vue reactive state
Convert imperative DOM updates to declarative Vue state using ref() and reactive(). Stop reading from the DOM and let Vue's reactivity system drive UI updates. Vue 3.5 improved reactivity memory usage by 56%. [src2, src3, src6]
// BEFORE: jQuery
let count = 0;
$('#counter').text(count);
$('#increment').on('click', function() {
count++;
$('#counter').text(count);
});
// AFTER: Vue 3 Composition API
import { createApp, ref } from 'vue';
const Counter = {
setup() {
const count = ref(0);
const increment = () => count.value++;
return { count, increment };
},
template: `
<div>
<span>{{ count }}</span>
<button @click="increment">Increment</button>
</div>
`
};
Verify: Remove the jQuery code for that widget, confirm the Vue version renders and behaves identically. Check Vue DevTools — state should be reactive.
5. Replace $.ajax with fetch or a Vue composable
Convert AJAX calls to the modern fetch API wrapped in a reusable composable. Composables are Vue's equivalent of React hooks. Use onWatcherCleanup() (Vue 3.5+) for automatic request cancellation inside watchers. [src3, src6]
// composables/useFetch.js
import { ref, onMounted, onUnmounted } from 'vue';
export function useFetch(url) {
const data = ref(null);
const error = ref(null);
const loading = ref(true);
let controller = null;
async function fetchData() {
controller = new AbortController();
loading.value = true;
error.value = null;
try {
const res = await fetch(url, { signal: controller.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
data.value = await res.json();
} catch (err) {
if (err.name !== 'AbortError') error.value = err.message;
} finally {
loading.value = false;
}
}
onMounted(fetchData);
onUnmounted(() => controller?.abort());
return { data, error, loading, refetch: fetchData };
}
Verify: Network tab shows the same API requests. Unmounting the component should abort in-flight requests.
6. Wrap jQuery plugins using custom directives
For plugins without Vue alternatives, wrap them in a Vue custom directive using the mounted and unmounted lifecycle hooks. [src5, src8]
// directives/v-chosen.js
import $ from 'jquery';
import 'chosen-js';
export const vChosen = {
mounted(el, binding) {
const $el = $(el);
$el.chosen({ width: '100%', ...binding.value });
$el.on('change', () => {
el.dispatchEvent(new Event('change'));
});
},
updated(el) {
$(el).trigger('chosen:updated');
},
unmounted(el) {
$(el).chosen('destroy');
}
};
// Register: app.directive('chosen', vChosen);
// Use: <select v-model="val" v-chosen="{ search_contains: true }">
Verify: Plugin initializes on mount, updates when Vue state changes, and destroys cleanly on unmount — no console errors or memory leaks.
7. Remove jQuery and clean up
Once all components are migrated, uninstall jQuery and remove all script tags. If migrating from jQuery 3.x, consider whether jQuery 4.0 compatibility changes affect your remaining code. [src3, src4, src7]
npm uninstall jquery
# Remove from HTML: <script src="jquery.min.js"></script>
grep -rn '\$(\|jQuery(' --include='*.js' --include='*.vue' --include='*.html'
Verify: grep -rn 'jquery' --include='*.json' --include='*.js' --include='*.vue' returns zero results. App runs without jQuery loaded. Bundle size should decrease by ~19.5–30 KB (gzipped).
Code Examples
JavaScript/Vue 3: Full page migration from jQuery to Vue SFC
Full script: javascript-vue-3-full-page-migration-from-jquery-t.js (46 lines)
// Input: A jQuery page with search form, results list, and loading spinner
// Output: Equivalent Vue 3 Single File Component with Composition API
// SearchPage.vue
// <script setup>
import { ref, computed } from 'vue';
const query = ref('');
const results = ref([]);
const loading = ref(false);
const error = ref(null);
const hasResults = computed(() => results.value.length > 0);
async function handleSearch() {
if (!query.value.trim()) return;
loading.value = true;
error.value = null;
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(query.value)}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
results.value = data.items;
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
}
// </script>
JavaScript/Vue 3: Bridge store for jQuery-Vue coexistence
Full script: javascript-vue-3-bridge-store-for-jquery-vue-coexi.js (31 lines)
// Input: A legacy jQuery page where Vue needs to read/write shared state
// Output: A reactive bridge that lets jQuery and Vue components share data
// bridge-store.js
import { reactive, watch } from 'vue';
export const bridgeStore = reactive({
user: null,
theme: 'light',
notifications: []
});
// jQuery side: window.bridgeStore = bridgeStore;
// Vue side: already reactive — import and use directly
watch(
() => bridgeStore.theme,
(newTheme) => {
document.body.setAttribute('data-theme', newTheme);
$(document).trigger('theme:changed', [newTheme]);
}
);
TypeScript/Vue 3: Type-safe composable replacing jQuery AJAX patterns
Full script: typescript-vue-3-type-safe-composable-replacing-jq.ts (51 lines)
// Input: jQuery $.ajax calls with consistent error handling
// Output: Type-safe Vue 3 composable with automatic loading/error state
// composables/useApi.ts
import { ref, type Ref } from 'vue';
interface UseApiReturn<T> {
data: Ref<T | null>;
error: Ref<string | null>;
loading: Ref<boolean>;
execute: (...args: unknown[]) => Promise<void>;
}
export function useApi<T>(
url: string | ((...args: unknown[]) => string),
options: RequestInit = {}
): UseApiReturn<T> {
const data = ref<T | null>(null) as Ref<T | null>;
const error = ref<string | null>(null);
const loading = ref(false);
// ... (see full script for complete implementation)
}
Anti-Patterns
Wrong: Direct DOM manipulation inside Vue components
// ❌ BAD — jQuery DOM manipulation inside a Vue component
const Counter = {
mounted() {
$('#count').text(0);
$('#increment').on('click', () => {
const current = parseInt($('#count').text());
$('#count').text(current + 1);
});
},
template: `
<div>
<span id="count">0</span>
<button id="increment">+1</button>
</div>
`
};
Correct: Use Vue reactive state for all UI updates
// ✅ GOOD — Vue reactivity drives the UI
const Counter = {
setup() {
const count = ref(0);
return { count };
},
template: `
<div>
<span>{{ count }}</span>
<button @click="count++">+1</button>
</div>
`
};
Wrong: jQuery event delegation inside Vue
// ❌ BAD — jQuery event delegation alongside Vue
const ListComponent = {
mounted() {
$(this.$el).on('click', '.list-item', function() {
$(this).toggleClass('selected');
});
},
template: `<ul><li class="list-item" v-for="item in items" :key="item.id">{{ item.name }}</li></ul>`
};
Correct: Handle events with Vue directives and reactive state
// ✅ GOOD — Vue handles events and state
const ListItem = {
props: ['item'],
setup() {
const selected = ref(false);
const toggle = () => { selected.value = !selected.value; };
return { selected, toggle };
},
template: `
<li :class="{ 'list-item': true, selected }" @click="toggle">
{{ item.name }}
</li>
`
};
Wrong: $.ajax inside Vue without lifecycle cleanup
// ❌ BAD — No cancellation, state update after unmount
const UserProfile = {
props: ['userId'],
setup(props) {
const user = ref(null);
watch(() => props.userId, (id) => {
$.ajax({
url: `/api/users/${id}`,
success: (data) => { user.value = data; }
});
}, { immediate: true });
return { user };
},
template: `<div>{{ user?.name }}</div>`
};
Correct: fetch with AbortController in a composable
// ✅ GOOD — Proper cleanup prevents memory leaks and race conditions
import { ref, watch, onUnmounted } from 'vue';
const UserProfile = {
props: ['userId'],
setup(props) {
const user = ref(null);
let controller = null;
watch(() => props.userId, async (id) => {
controller?.abort();
controller = new AbortController();
try {
const res = await fetch(`/api/users/${id}`, { signal: controller.signal });
user.value = await res.json();
} catch (err) {
if (err.name !== 'AbortError') console.error(err);
}
}, { immediate: true });
onUnmounted(() => controller?.abort());
return { user };
},
template: `<div>{{ user?.name }}</div>`
};
Wrong: Replacing entire jQuery pages at once with Vue
// ❌ BAD — Wrapping an entire jQuery page in one Vue app
const app = createApp({
mounted() {
initDatePickers();
initModals();
initDataTables();
initCharts();
},
template: `<div id="entire-page" v-html="legacyHtml"></div>`
});
Correct: Mount multiple small Vue apps into the existing page
// ✅ GOOD — Replace one widget at a time with its own Vue app
const searchApp = createApp(SearchWidget);
searchApp.mount('#search-mount');
const chartApp = createApp(ChartWidget);
chartApp.mount('#chart-mount');
// Rest of the page is still managed by jQuery
// Remove jQuery only after ALL widgets are migrated
Wrong: Using $refs like jQuery selectors
// ❌ BAD — Treating refs as jQuery-style DOM query shortcuts
const FormComponent = {
mounted() {
this.$refs.nameInput.style.border = '2px solid red';
this.$refs.nameInput.value = 'default';
this.$refs.submitBtn.disabled = true;
},
template: `
<form>
<input ref="nameInput" />
<button ref="submitBtn">Submit</button>
</form>
`
};
Correct: Use reactive state and directive bindings
// ✅ GOOD — Let Vue's reactivity system manage everything
import { ref, computed } from 'vue';
const FormComponent = {
setup() {
const name = ref('default');
const hasError = ref(true);
const canSubmit = computed(() => name.value.length > 0 && !hasError.value);
return { name, hasError, canSubmit };
},
template: `
<form>
<input v-model="name" :class="{ error: hasError }" />
<button :disabled="!canSubmit">Submit</button>
</form>
`
};
Common Pitfalls
- Vue and jQuery both managing the same DOM node: Vue's virtual DOM and jQuery's direct DOM manipulation conflict, causing UI state to desync. When Vue re-renders, it overwrites jQuery's changes. Fix: Give each library its own DOM subtree — Vue renders into mount points that jQuery never touches. [src1]
- Forgetting to destroy jQuery plugins on Vue unmount: jQuery plugins attach event listeners and create DOM elements that persist after Vue unmounts, causing memory leaks and ghost UI. Fix: Always clean up in
unmountedoronUnmounted:$(el).plugin('destroy'). [src5, src8] - Big-bang rewrite instead of incremental migration: Stalls feature development for months and introduces regression risk. Fix: Migrate one widget/page at a time. Ship each migration to production before starting the next. A typical 50-page app takes 3–6 months incrementally vs. 6–12 months for a rewrite. [src3, src4]
- Not extracting implicit state from the DOM first: In jQuery apps, the DOM is the state — hidden inputs,
data-*attributes, element classes. Fix: Before migrating UI, extract this implicit state into JavaScript variables, a Pinia store, or a shared reactive object. [src4] - Using
v-htmlto inject jQuery-rendered HTML: Bypasses Vue's reactivity system and is an XSS risk. Fix: Convert jQuery-generated HTML to Vue templates with proper data binding. Sanitize with DOMPurify if raw HTML is unavoidable. [src2] - Mixing
$(document).ready()with Vue mounting: Creates unnecessary coupling and race conditions. Fix: Use Vue'sonMountedlifecycle hook for initialization. For bridge scenarios, mount Vue apps from a plain<script>after the DOM is ready. [src3] - Ignoring Vue DevTools during migration: Without DevTools, debugging reactivity issues is nearly impossible during coexistence. Fix: Install Vue DevTools browser extension immediately to inspect component state and track event flow. [src2]
- Using jQuery 4.0 slim build with code that depends on $.Deferred: jQuery 4.0 slim removes Deferreds and Callbacks. Fix: Use the full jQuery 4.0 build during migration or convert Deferreds to native Promises first:
const promise = new Promise((resolve, reject) => { /* ... */ }). [src7]
Diagnostic Commands
# Count remaining jQuery references in the codebase
grep -rn '\$(\|jQuery\|\.ajax\|\.on(' --include='*.js' --include='*.vue' --include='*.html' | wc -l
# Find jQuery script tags in HTML files
grep -rn 'jquery\|jQuery' --include='*.html' | grep -i 'script'
# Check if jQuery 4.0 removed APIs are in use
grep -rn 'jQuery\.isArray\|jQuery\.parseJSON\|jQuery\.trim\|jQuery\.type\|jQuery\.now' --include='*.js' | wc -l
# Check bundle size for jQuery (Vite)
npx vite build 2>&1 | grep -i jquery
# Verify Vue is rendering (browser console)
# document.querySelectorAll('[data-v-app]').length
# List all mounted Vue app instances (browser console)
# document.querySelectorAll('.__vue_app__')
# Check for memory leaks (Chrome DevTools)
# Performance tab -> Record -> Interact -> Check heap snapshots for detached DOM nodes
# Verify Vue 3.5 features are available
# In browser console: Vue.version
Version History & Compatibility
| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| Vue 3.6 (beta, Dec 2025) | Beta / unstable | Vapor Mode (no virtual DOM), @vue/reactivity refactored on alien-signals | 100% opt-in via <script setup vapor> + createVaporApp; interop with vDOM apps via vaporInteropPlugin. Stable projected Q4 2026 — do not target for production migrations yet. [src9] |
| Vue 3.5 (Sep 2024) | Current (stable) | Reactive props destructure stable, useTemplateRef(), onWatcherCleanup() | Recommended for new migrations. 56% less memory usage. [src6] |
| Vue 3.4 (Dec 2023) | Maintained | defineModel stable | Simplifies v-model on custom components |
| Vue 3.3 (May 2023) | Maintained | defineOptions macro, generic components | Minimum recommended version for new migrations |
| Vue 3.0–3.2 (2020–2022) | Maintenance | Composition API, <script setup> | <script setup> stabilized in 3.2 — use it |
| Vue 2.7 (Jul 2022) | EOL (Dec 2023) | Backported Composition API | Do not target Vue 2 for new migrations |
| jQuery 4.0 (Jan 2026) | Current | Removed $.isArray, $.parseJSON, $.trim, $.type, $.now; slim removes Deferreds; ES modules | Can coexist with Vue 3. Check upgrade guide. [src7] |
| jQuery 3.x (2016–2024) | Maintained | Removed deprecated APIs | Can coexist with Vue 3 during migration |
| jQuery 1.x–2.x | EOL | IE-specific code | Upgrade to 3.x/4.x first, or remove directly |
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Building component-driven SPAs with reactive data | Static sites with 2–3 event handlers | Vanilla JS or Alpine.js |
| Team values single-file components and template syntax | Team strongly prefers JSX | React (see jquery-to-react) |
| Codebase uses server-rendered HTML (PHP, Django, Rails) | Project is already React/Angular | Stay with current framework |
| Need incremental migration without build step (CDN) | jQuery codebase is <500 lines total | Replace jQuery calls with vanilla JS |
| App has complex forms requiring two-way binding | Project is a throwaway prototype | Keep jQuery |
| Team is small and needs gentle learning curve | Need SSR with streaming and React Server Components | React/Next.js |
Important Caveats
- Vue 3 requires the full build (
vue.global.jsorvue.esm-browser.js) if using in-DOM templates (HTML as template source). The runtime-only build does not include the template compiler. This is critical for incremental migration where Vue uses existing HTML. - jQuery 3.x
$.Deferredis not fully Promise/A+ compliant — convert to native Promises orasync/awaitwhen migrating AJAX calls to Vue composables. jQuery 4.0 slim build removes Deferreds entirely. [src7] - Vue's
<Transition>component only works with elements toggled byv-iforv-show— it does not replace jQuery's.animate()for arbitrary CSS property tweening. Use CSS transitions or GSAP for complex animations. - Bundle size during coexistence: Vue 3 adds ~33 KB gzipped alongside jQuery's ~19.5–30 KB (depending on version/build). Total overhead is temporary. Remove jQuery as soon as all references are migrated.
- Vue DevTools does not show components mounted via
createApp().mount()unless the app is stored in a variable accessible from the page. During migration, keep app references for debugging:window.__vueApps = { search: searchApp }. - jQuery 4.0 migrated source from AMD to ES modules and switched to Rollup for packaging. If your build tool already bundles jQuery 3.x via AMD, the jQuery 4.0 upgrade may require build config changes (Webpack/Vite). [src7]
- Vue 3.6 (beta as of late 2025/2026) introduces Vapor Mode — a compile-time strategy that eliminates the virtual DOM for opted-in components, reaching Solid/Svelte-class performance. It is 100% opt-in (
<script setup vapor>,createVaporApp,vaporInteropPluginfor mixing with vDOM components) and deliberately omits the Options API, global properties, andgetCurrentInstance(). It is feature-complete but still unstable; target Vue 3.5 for production jQuery migrations and adopt Vapor Mode only for isolated new components. [src9] - Vue 3.6 also refactors
@vue/reactivityon top of alien-signals, further improving reactivity throughput and memory use over the 3.5 baseline. This is transparent to migration code —ref/reactive/computedAPIs are unchanged. [src9]