How We Refactored 500k LOC from Vue 3.3 to 3.4 with Vite 6 and TypeScript 5.6
Large-scale codebase migrations are often fraught with risk: downtime, regressions, and developer productivity loss. When our team decided to upgrade our 500,000-line-of-code (LOC) Vue application from Vue 3.3 to 3.4, pair the upgrade with Vite 6 and TypeScript 5.6, we knew we needed a meticulous, incremental approach to avoid disruption. This article walks through our planning, execution, and lessons learned from one of the largest frontend migrations our team has ever undertaken.
Why We Upgraded
Our application, a enterprise resource planning (ERP) tool used by 10k+ daily active users, was running on Vue 3.3, Vite 5, and TypeScript 5.5. We had three core drivers for the upgrade: first, Vue 3.4 stabilized key features we’d been testing in experimental mode, including defineModel and improved reactive performance. Second, Vite 6 promised 30% faster build times and better environment isolation for our micro frontend modules. Third, TypeScript 5.6 added stricter type narrowing and native support for the ECMAScript decorators proposal, which aligned with our goal to improve type coverage across the codebase.
Pre-Migration Planning
We spent 3 weeks on planning before writing a single migration line. First, we audited our entire codebase to map usage of deprecated Vue 3.3 APIs, Vite 5 legacy config options, and TypeScript 5.5 patterns that would break in 5.6. We used tools like eslint-plugin-vue 9.x, vite-plugin-checker, and a custom AST script to generate a report of high-risk areas: 12 modules with heavy use of legacy v-model syntax, 8 modules using Vite’s deprecated build.rollupOptions patterns, and 42 files with loose type annotations that would fail TS 5.6’s stricter checks.
We created a migration runbook with clear ownership for each module, set up a dedicated staging environment that mirrored production traffic, and added pre-commit hooks to block merges that used deprecated APIs. We also adopted an incremental migration strategy: no big-bang deploy, instead migrating one module at a time, running full test suites after each merge, and validating performance metrics before moving to the next module.
Vue 3.3 to 3.4 Migration
Vue 3.4’s breaking changes were relatively minimal compared to previous major versions, but the 500k LOC scale meant even small fixes added up. The largest change was replacing experimental defineModel usage (which required the experimentalDefineModel flag in 3.3) with the stabilized version, removing the flag from our Vue config. We wrote a custom Babel plugin to auto-update 1,200+ v-model bindings across the codebase, reducing manual effort by 80%.
We also removed usage of deprecated Vue.extend in favor of defineComponent, updated our Pinia stores to use Vue 3.4’s improved reactive tracking, and fixed edge cases in our custom directive implementations that relied on 3.3’s legacy reactivity behavior. Our 85% unit test coverage and Cypress end-to-end (E2E) suite caught 14 regressions during this phase, all of which were fixed before merging to main.
Vite 6 Upgrade
Vite 6’s breaking changes centered on legacy Node.js support (dropping Node 16, which we were still using in CI) and a revamped plugin API. We first upgraded our CI environment to Node 20, then updated our vite.config.ts to replace deprecated @vitejs/plugin-vue 4.x with 5.x, which added native support for Vue 3.4’s SFC features. We also replaced our custom Rollup plugin configurations with Vite 6’s new environment API, which simplified our micro frontend build pipeline.
Post-upgrade, we saw a 32% reduction in production build times, a 12% smaller average bundle size due to improved tree-shaking, and faster HMR (hot module replacement) in development: average HMR latency dropped from 450ms to 180ms. We did encounter one edge case with our environment variable handling, which Vite 6 now validates more strictly, but a small config update resolved it.
TypeScript 5.6 Integration
TypeScript 5.6’s stricter default moduleResolution (set to bundler by default) and improved type narrowing required the most manual effort. We updated our tsconfig.json to align with 5.6’s recommended settings, then used ts-migrate to auto-fix 2,100+ loose type annotations across the codebase. We also added custom type guards for Vue 3.4’s new useTemplateRef API, which improved type safety for our template ref usage.
Type coverage across the codebase improved from 78% to 92% post-migration, and we saw a 40% reduction in type-related bug reports from the development team in the month following the upgrade. We did have to update 18 third-party library type definitions that were not yet compatible with TS 5.6, but most maintainers had already released patches by the time we migrated.
Testing and Rollout
After all modules were migrated, we ran a full regression suite: 12,000+ unit tests, 450 E2E tests, and 2 weeks of load testing on staging. We then rolled out the upgrade to production incrementally: 10% of traffic for 48 hours, 50% for 24 hours, then 100%. We monitored error rates, LCP (largest contentful paint), and FID (first input delay) via Datadog, and saw no statistically significant changes in any metric post-launch.
Lessons Learned
The migration took 8 weeks total, with zero production downtime and minimal disruption to ongoing feature development. Key takeaways for large-scale frontend migrations: (1) Invest heavily in pre-migration auditing and automation to reduce manual effort. (2) Incremental rollout is non-negotiable for codebases over 100k LOC. (3) High test coverage is the only way to catch regressions early. (4) Communicate changes to the broader development team weekly to avoid confusion. (5) Document all custom fixes and edge cases for future upgrades.
Today, our team is reaping the benefits of the upgrade: faster development cycles, better type safety, and access to Vue 3.4’s latest features. For teams planning similar large-scale migrations, we recommend starting with a small pilot module, iterating on your process, and scaling up once you’ve ironed out early issues.
Top comments (0)