Vue 2 to Vue 3 migration: what changes and what to plan

Quick answer: Don’t rewrite first if your Vue 2 product is stable. Inventory dependencies, use
@vue/compatfor plain Vue apps or a Nuxt-specific migration path for Nuxt apps, then remove warnings until the app runs on Vue 3 behavior.
Your Vue 2 app still ships. The tests pass. Customers are happy.
Then a package upgrade lands, and its next version needs Vue 3. The release stalls while the team argues whether this is a package bump, a migration-build cleanup, a Nuxt project, or a rewrite.
That’s the real risk in a Vue 2 to Vue 3 migration. Not the syntax. The risk is choosing the wrong path before you know what will break.
By Sudipto Mondal, Senior Software Engineer at VueSquad. Sudipto is a full-stack developer with 5+ years of experience across SaaS platforms, eCommerce systems, and custom software.
The practical answer: migrate, stage, or rewrite?
You need a decision before you touch package.json.
Vue 2 reached end of life on December 31, 2023. Treating it as your long-term product line is a risk, even if the app still runs.
The last Vue 2 release was Vue 2.7.16. The question is no longer, “Should we ever move?” It’s, “How do we move without freezing roadmap work?”
If your app is stable, the architecture is healthy, and key dependencies have Vue 3 paths, migrate in place. That is our default recommendation at VueSquad. If you need senior help planning or doing that work, we provide Vue 2 to Vue 3 migration help inside your existing tools and workflow.
If the app has heavy technical debt, don’t force an in-place migration. A fresh Vue 3 or Nuxt 3 codebase can be safer. That is especially true when outdated libraries, poor structure, or broad rewrites would touch most screens.
Use this decision rule first
Ask these questions before you estimate anything:
- Is this plain Vue or Nuxt? Nuxt has its own migration path.
- Do the main dependencies support Vue 3? UI libraries, router, state, plugins, charts, forms, and analytics all count.
- Are tests strong enough to protect user flows? If not, add tests before large changes.
A good migration starts as an inventory project, not a code-editing sprint.
When staying on Vue 2 is acceptable for a short time
Staying on Vue 2 can be acceptable while you prepare funding, staff, tests, or dependency replacements.
It is not acceptable as “we’ll think about it later” if your product depends on package upgrades, security patches, hiring, browser changes, or Nuxt module updates.
Use the delay to build a migration backlog. Don’t use it to avoid the decision.
Vue 2 vs Vue 3: the changes that affect planning
The difference between Vue 2 and Vue 3 is not only Options API versus Composition API.
Vue 3 changes the runtime, compiler, app setup, lifecycle behavior, slots, v-model, transition class names, and several APIs. Use the official Vue 2 to Vue 3 breaking changes as your checklist.
For planning, scan these areas first:
- global app setup and plugins
- component library wrappers
- slots and scoped slots
v-modeland.syncpatterns- custom directives
- filters and removed APIs
- transition class names
- class components and decorators
- direct prop mutation
- tests that mount components with old global mocks
Don’t turn every component into Composition API on day one
Vue 3 supports the Composition API and <script setup>, but migration should not become a rewrite of every component.
If a component is stable and boring, keep it boring. Change it only when the old pattern blocks Vue 3 behavior, TypeScript safety, testability, or future maintenance.
Teams get into trouble when they combine five projects into one release: Vue 3 migration, TypeScript adoption, UI redesign, state rewrite, and folder restructure. Pick the smallest set of changes that gets you safely onto Vue 3.
A small code pattern that affects real work
Here is a common Vue 2 parent-child update pattern:
<!-- Vue 2 pattern -->
<template>
<UserNameInput :value="name" @input="name = $event" />
</template>
<script>
export default {
data() {
return { name: '' }
}
}
</script>
In Vue 3, make the model contract explicit:
<!-- Vue 3 parent -->
<template>
<UserNameInput v-model="name" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
const name = ref('')
</script>
<!-- UserNameInput.vue -->
<template>
<input
:value="modelValue"
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
/>
</template>
<script setup lang="ts">
defineProps<{ modelValue: string }>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
</script>
This is not about nicer syntax. It gives tests and parent components a clear update contract.
@vue/compat and migration tools are helpers, not a converter
Tools can reduce risk. They cannot understand your product behavior.
What @vue/compat does
@vue/compat is the Vue 3 migration build. It provides configurable Vue 2-compatible behavior. It runs in Vue 2 mode by default and emits runtime warnings for changed or deprecated behavior through the @vue/compat migration build.
The setup has a few moving parts:
- match
vueand@vue/compatversions - replace
vue-template-compilerwith@vue/compiler-sfc - alias
vueto@vue/compat - enable compatibility compiler options
- use
compatConfigper component as you move toward Vue 3 behavior
Warnings are not noise. They are your backlog.
Why there is no safe one-click Vue 2 to Vue 3 converter
Searchers ask for a Vue 2 to Vue 3 migration tool because they want a clean answer. The honest answer is simple: codemods help, but they do not finish the job.
The vue-codemod project presents automated changes as a starting point, and its docs say runtime behavior still needs checking through vue-codemod.
A converter cannot decide whether your date picker has a Vue 3 release. It cannot prove checkout still works. It cannot resolve a Nuxt hydration mismatch.
Exit criteria for the migration build
Don’t leave the app in compatibility mode forever.
Your exit criteria should be:
- no compatibility warnings in normal user flows
- known transition-class issues checked manually
- key routes tested in production-like builds
- Vue 3 compiler mode enabled
- old package shims removed
- migration notes turned into team rules
If nobody owns the warnings, compatibility mode becomes the new legacy system.
Nuxt 2 to Nuxt 3 is a separate migration plan
If your app is Nuxt, plain Vue advice is not enough.
Why Nuxt changes the migration route
Nuxt migration includes more than a Vue runtime upgrade. It moves from Vue 2 to Vue 3. It also moves from webpack 4/Babel to Vite or webpack 5/esbuild, plus Nitro’s standalone server model in the Nuxt migration overview.
Nuxt Bridge can be a useful middle step. It lets a Nuxt 2 app adopt selected Nuxt 3 features before a full migration.
But don’t assume @vue/compat solves Nuxt 3. Nuxt’s docs state that the Vue 3 migration build cannot currently be used with Nuxt 3 in the Nuxt configuration migration guide.
Bridge, direct migration, or rebuild?
Pick Bridge when the app needs staged modernization and still has Nuxt 2 constraints.
Pick direct Nuxt 3 migration when modules are healthy, pages are clear, and your team can test SSR or prerendered routes.
Pick a rebuild when the old Nuxt app is deeply tangled: unclear data fetching, stale modules, broken layouts, and content rendering that needs a new model anyway.
For Nuxt teams, we offer Nuxt migration support when the Vue 2 migration is really a Nuxt 2 migration.
Common Nuxt blockers: modules, content, and hydration
Nuxt page and layout conventions change. Nuxt 2’s layout component moves from <Nuxt> to slots, and nested pages move from <NuxtChild> to <NuxtPage> in the Nuxt pages and layouts migration guide.
<!-- Nuxt 2 layout -->
<template>
<div>
<SiteHeader />
<Nuxt />
</div>
</template>
<!-- Nuxt 3 layout -->
<template>
<div>
<SiteHeader />
<slot />
</div>
</template>
Nuxt modules and plugins need their own inventory. Nuxt’s module docs say Nuxt 3 plugins are not fully backward compatible with Nuxt 2 plugins, and modules may need rewriting under the Nuxt module migration guidance.
Hydration mismatches are one of the most common blockers we see in Nuxt production migrations. Hydration is the browser step where client Vue takes over server-rendered HTML. If server and client output differ, you get warnings, broken interactivity, or page-specific bugs.
Run this migration inventory before you touch package.json
The inventory prevents bad estimates.
Dependency inventory
Classify every dependency as ready, replace, rewrite, or remove. Ask for proof, not hope.
| Area | What to check | Why it affects the plan | Evidence to collect |
|---|---|---|---|
| Framework | Vue version, Nuxt version | Determines plain Vue or Nuxt path | Local build, migration docs |
| Router | Route APIs, guards, tests | Routing bugs break core flows | Passing route tests |
| State | Vuex, Pinia, custom stores | Store changes touch many screens | Store list and usage map |
| UI library | Components, themes, forms, tables | Often the biggest hidden cost | Official Vue 3 support docs |
| Build tool | Vue CLI, webpack, Vite | Compiler setup affects migration build | Working production build |
| Test tooling | Unit, component, E2E | Protects behavior during edits | Critical-flow test report |
| Plugins | Global properties, analytics, maps | Old plugin APIs often break | Plugin owner and replacement path |
| SSR/SSG | Data fetching, hydration, prerender | Nuxt bugs appear after build | Preview deployment |
| Directives | Custom DOM behavior | Directive hooks may need updates | Directive usage list |
Vue’s migration recommendations also tell teams to consider ecosystem moves. That includes Vue CLI to Vite, Vuex to Pinia, Vetur to Volar, and vue-tsc for TypeScript in the framework-level recommendations.
Code-pattern inventory
Search for patterns that create migration work:
- filters
.syncthis.$children- direct prop mutation
- global event buses
- class components
- implicit slot behavior
- custom directives
- components that depend on DOM timing
- large components with unclear ownership
We’ve seen outside Vue codebases where the hard part was not Vue 3 syntax. It was large components, direct prop mutation, and unclear communication between components, composables, and stores.
Test and release inventory
Say your app has many components, many dependencies, and several critical flows.
Turn that into a backlog like this:
- dependency decisions
- flow tests that must pass before release
- component groups by feature owner
- one spike for build setup
- one spike for UI library risk
- one release plan with rollback
At VueSquad, migration discovery starts with one split: do you want a Vue 3 compatibility migration, or a full modernization? Those are different projects.
Choose the migration path: in-place, Bridge, direct upgrade, or fresh build
Now turn the inventory into a choice.
| Path | Best when | Avoid when | Rollback ability | Main trade-off | Exit condition |
|---|---|---|---|---|---|
| Stay on Vue 2 briefly | You need time to staff, test, or replace blockers | You treat it as the product future | High | Risk keeps growing | Migration backlog approved |
@vue/compat | Plain Vue app, healthy structure, many components | Nuxt 3 is the target path | Medium | Warnings must be owned | Compat warnings removed |
| Nuxt Bridge | Nuxt 2 app needs staged modernization | You want to skip intermediate states | Medium | Adds a temporary state | Ready for Nuxt 3 migration |
| Direct migration | Tests are strong and dependencies are ready | Many dependencies are unknown | Lower | More upfront change | Production build and flows pass |
| Fresh Vue 3 or Nuxt 3 build | Current app blocks change | Size is the only concern | Lower | More parity work | New app replaces old flows |
Size alone is not a rewrite reason. A large, well-tested app with healthy dependencies can be safer to migrate than rebuild.
What a small Nuxt content-site migration looked like
We migrated an anonymized US music-lesson marketplace’s content-heavy Nuxt site from Nuxt 2.15 and Vue 2 to Nuxt 3.10 and Vue 3.4.
The site was roughly 40 pages. One VueSquad engineer did the migration while the client’s developer reviewed and merged work. The work took about one week.
The main blocker was not Vue component syntax. It was Nuxt Content v1 to v2 markdown rendering. Prerendering and hydration fixes came next.
When a rewrite is the honest choice
Choose a fresh build when the migration would touch most of the app anyway.
Common signs:
- old UI library with no clean path
- no useful tests
- hidden business logic inside views
- broken SSR or prerender assumptions
- global plugins used everywhere
- features the product no longer needs
- layout and state structure that fights every change
A rewrite is not failure. It is failure only when you call it a migration and skip parity planning.
Failure modes to watch during the migration
Most migration failures have a clear smell. Catch them early.
| Symptom | Likely root cause | How to confirm it | Fix |
|---|---|---|---|
| App builds but pages warn in console | Compat warnings ignored | Test real routes, not just build | Turn warnings into tickets |
| Layout works locally but fails after deploy | SSR or hydration mismatch | Compare server HTML and client state | Move browser-only code behind client checks |
| Form library breaks everywhere | Vue 2 UI dependency | Check official Vue 3 path | Upgrade, replace, or isolate |
| Nuxt pages render blank | Layout/page convention changed | Check layout and nested page usage | Replace old Nuxt components and route APIs |
| Tests fail before migration starts | Test setup tied to old globals | Run test suite before changes | Update test harness first |
| Feature teams keep reintroducing old patterns | No migration rules | Review new PRs for changed APIs | Add lint rules and review checklist |
Teams on r/vuejs often describe the migration as warning cleanup plus full-flow testing, not a simple install command. That matches what we see in real projects, especially where dependencies are old.
Staffing, cost, and rollout plan
A migration costs less when the right people own the right risks.
Compare staffing options first
No reliable universal market price or timeline applies to Vue migrations. Effort depends on app size, dependency health, test coverage, Nuxt usage, and release risk.
Use a simple internal formula:
Monthly cost = people assigned × their monthly cost + delayed roadmap work
| Option | Best fit | Internal effort needed | Main risk |
|---|---|---|---|
| In-house team | Strong Vue team with protected time | High | Roadmap work slows |
| Permanent hire | Long-term Vue investment | Medium after onboarding | Hiring takes time |
| Freelancer | Bounded tasks with clear specs | High review effort | Continuity varies |
| Staff augmentation | You own architecture and need capacity | Medium | Needs strong internal direction |
| Specialist Vue/Nuxt team | Complex migration or missing senior Vue depth | Medium | Must fit your workflow |
| Generalist agency | Larger web program with many stacks | High Vue review effort | Vue/Nuxt edge cases missed |
Where VueSquad fits
VueSquad is one option when you need senior Vue engineers who join your tools: GitHub, GitLab, Linear, Jira, and Slack. You can also hire senior Vue engineers for migration work and ongoing product development.
| Plan | Monthly price | Monthly hours | Best fit |
|---|---|---|---|
| Full-Time Senior Vue Engineer | $2,499 per month | 160 hours per month | Ongoing product development, SaaS platforms, large or evolving codebases, migrations and long-term maintenance |
| Part-Time Senior Vue Engineer | $1,499 per month | 80 hours per month | Feature support, refactoring, performance optimization, UI modernization, and migration assistance |
| Dedicated Vue Squad | Custom pricing | Custom | Multiple senior Vue engineers with engineering-lead oversight, shared standards, design system support, sprint planning, and weekly reporting |
Every plan has monthly billing, flat rate, no onboarding fees, no minimum commitment, and pause or cancel anytime. Clients can start within 48 hours in most cases, meet and approve the engineer before onboarding, and work with overlapping US and EU hours.
You can review current VueSquad pricing before you compare this with your internal cost.
Rollout checkpoints
Use checkpoints that reduce release risk:
- Inventory approved.
- Target path chosen.
- Build setup working.
- Critical-flow tests passing before migration.
- Migration warnings assigned.
- Preview deployment reviewed.
- Production rollout has rollback steps.
- Old compatibility code removed.
Don’t ship because “the build is green.” Ship because the flows your users pay for are proven.
Your next step
If your app is stable, start with an inventory and plan an in-place migration. If it’s Nuxt, split that into a Nuxt migration plan before you estimate.
If you want senior Vue engineers to help choose the path, clean up warnings, and ship safely, you can plan your Vue 2 to Vue 3 migration with VueSquad.
Frequently asked questions
Is Vue 2 end of life?
Yes. Vue 2 is outside the official support line, as covered near the top. You can keep a Vue 2 app running, but treat that as a short-term state while you plan the move.
What is the difference between Vue 2 and Vue 3?
The practical difference is runtime, compiler, app setup, tooling, and ecosystem behavior. Composition API matters for new development, but migration planning should focus on breaking changes, dependencies, tests, and release risk.
Can I use Vuetify 2 with Vue 3?
Treat Vuetify 2 as a migration dependency, not a small package detail. Vuetify says Vuetify 2 has reached EOL and recommends planning a move to Vuetify 3 through its Vuetify 2 EOL notice.
Is Vue 3 faster than Vue 2?
Don’t sell the migration to leadership as a guaranteed speed win. Benchmark your own app before and after using the same routes, data, build mode, and hosting setup.
Can a Vue 2 to Vue 3 migration tool convert my app?
No safe one-click converter exists for a real product. Tools and codemods can change code patterns, but they cannot prove dependency support, Nuxt behavior, checkout flows, hydration, or production data cases.
Can I use Vue 2 components in a Vue 3 project?
Some ordinary Options API components may move with small changes. The risk is usually around plugins, global APIs, class components, UI libraries, state, slots, and old two-way binding patterns.