Most migration guides assume a relatively modern codebase. They start with Vue CLI 5, recent Node versions, maintained dependencies, and a straightforward path toward Vite.
That was not my reality.
The project I inherited was a legacy Vue 2 application built on Webpack 2. The ecosystem contained deprecated loaders, abandoned plugins, outdated Babel configurations, CommonJS packages, custom aliases, and years of accumulated technical debt. Rewriting the application was not an option, and upgrading every dependency simultaneously would have introduced an unacceptable amount of risk.
The objective was simple: replace Webpack 2 with Vite while preserving the existing application architecture and maintaining compatibility with legacy dependencies.
This article focuses on the technical process itself. Rather than discussing modernization from a high level, I will walk through the exact migration strategy, the compatibility issues encountered, and the solutions that allowed the project to continue operating without a complete rewrite.
Understanding What Webpack Is Actually Doing
Before installing Vite, it is essential to understand that Webpack is not merely bundling JavaScript. In most Vue 2 applications that have existed for several years, Webpack is responsible for resolving aliases, transpiling code, injecting environment variables, loading assets, compiling stylesheets, handling code splitting, and often compensating for browser incompatibilities.
A common mistake is attempting to install Vite and immediately delete the existing Webpack configuration.
Instead, start by mapping every responsibility currently performed by Webpack.
Analyze files such as:
webpack.config.js
webpack.prod.js
webpack.dev.js
.babelrc
package.json
Document:
- Aliases
- Loaders
- Plugins
- Environment variables
- Babel presets
- Polyfills
- Dynamic imports
- Asset handling
- Sass/Less configurations
Only after understanding these responsibilities should the migration begin.
Establishing a Stable Node Environment
Legacy Vue 2 projects often depend on old Node versions.
Before introducing Vite, identify the highest Node version capable of running the project without breaking dependencies.
A typical scenario looks like this:
{
"engines": {
"node": "8.x"
}
}
Attempting to jump directly from Node 8 to Node 22 while simultaneously replacing Webpack introduces too many variables.
Instead:
- Upgrade Node incrementally.
- Verify build stability.
- Resolve dependency incompatibilities.
- Only then introduce Vite.
Many failures blamed on Vite are actually caused by outdated dependencies that cannot execute in modern Node environments.
Installing Vite Without Removing Webpack
The safest migration strategy is running both build systems simultaneously.
Install Vite and Vue 2 support:
npm install vite vite-plugin-vue2 --save-dev
Create a dedicated Vite configuration:
import { defineConfig } from 'vite'
import { createVuePlugin } from 'vite-plugin-vue2'
export default defineConfig({
plugins: [
createVuePlugin()
]
})
At this stage, Webpack remains untouched.
The objective is creating a parallel build environment rather than replacing the existing one immediately.
Your scripts may temporarily look like:
{
"scripts": {
"dev": "webpack-dev-server",
"dev:vite": "vite",
"build": "webpack",
"build:vite": "vite build"
}
}
This allows side-by-side validation throughout the migration process.
Migrating Aliases
Most large Vue 2 applications rely heavily on aliases.
A typical Webpack configuration might contain:
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
'@services': path.resolve(__dirname, 'src/services')
}
}
These aliases must be replicated within Vite:
import path from 'path'
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/components'),
'@services': path.resolve(__dirname, './src/services')
}
}
})
Failure to mirror aliases correctly usually produces hundreds of import resolution errors.
This should be one of the first migration tasks completed.
Replacing Webpack-Specific Environment Variables
Webpack projects frequently depend on:
process.env.API_URL
Vite uses a different model based on native ESM.
Before:
const apiUrl = process.env.API_URL
After:
const apiUrl = import.meta.env.VITE_API_URL
Environment files must also be updated:
VITE_API_URL=https://api.company.com
One important detail is that Vite only exposes variables prefixed with:
VITE_
Variables without this prefix will not be available in client-side code.
Refactoring Dynamic Require Statements
One of the largest migration blockers in legacy Vue applications is the extensive use of runtime require statements.
Webpack tolerated patterns like:
const page = require('./pages/' + pageName)
or
const component = require(path)
Vite cannot statically analyze these imports.
The modern replacement is:
const pages = import.meta.glob('./pages/*.vue')
Usage:
const page = pages[`./pages/${pageName}.vue`]
This change is often unavoidable.
Projects with extensive dynamic loading mechanisms usually require dedicated refactoring during migration.
Migrating Asset Imports
Webpack loaders often hide complexity.
Examples include:
import logo from './logo.png'
import icon from './icon.svg'
Fortunately, most static assets work immediately in Vite.
However, custom loaders require investigation.
Common examples include:
file-loader
url-loader
svg-inline-loader
raw-loader
Many become unnecessary because Vite provides native handling.
When custom behavior exists, identify whether it can be replaced through:
- Vite plugins
- Native imports
- Asset URL transformations
Never assume loader behavior automatically transfers to Vite.
Handling CommonJS Dependencies
Legacy Vue 2 projects often depend on libraries published years before ES Modules became standard.
Examples:
module.exports = library
or
exports.default = library
Vite can optimize many CommonJS packages automatically.
When issues occur:
export default defineConfig({
optimizeDeps: {
include: [
'legacy-library',
'old-plugin'
]
}
})
For problematic packages:
import library from 'legacy-library'
export default library
Creating compatibility wrappers often avoids widespread refactoring.
Fixing Global Variables and Polyfills
Webpack silently injected several browser polyfills.
Vite does not.
Typical migration errors include:
process is not defined
Buffer is not defined
global is not defined
Solutions vary depending on usage.
For Buffer:
import { Buffer } from 'buffer'
window.Buffer = Buffer
For process:
npm install process
import process from 'process'
window.process = process
Polyfills should be added intentionally rather than globally whenever possible.
Reviewing Babel Requirements
Many Vue 2 applications contain complex Babel configurations inherited from older browser support requirements.
Example:
{
"presets": [
["env", {
"modules": false
}]
]
}
Before migrating, determine whether Babel is still necessary.
In many cases:
- Legacy IE support has been dropped.
- Modern browsers are sufficient.
- Several transformations become unnecessary.
Reducing Babel complexity often simplifies the migration significantly.
Converting Development Workflows
One of the most visible differences after migration is the development experience.
Webpack:
npm run dev
may require 20–60 seconds before changes appear.
Vite:
npm run dev:vite
typically starts almost instantly because dependencies are pre-bundled and source files are served as native ES modules.
The perceived performance improvement is often the first confirmation that the migration is succeeding.
Production Validation
A successful development server means very little.
The final phase should focus on production validation.
Review:
- Routing
- Authentication
- API communication
- File uploads
- Lazy loading
- Asset generation
- Source maps
- Environment variables
Compare generated bundles.
Analyze network requests.
Verify route-level code splitting.
Inspect console warnings.
Most migration issues appear during production builds rather than development execution.
Final Thoughts
The hardest part of migrating a Vue 2 application from Webpack 2 to Vite is not configuring Vite itself. The real challenge lies in uncovering years of implicit assumptions hidden within a mature codebase.
Webpack frequently compensates for architectural decisions that developers no longer remember making. Dynamic imports, CommonJS dependencies, deprecated loaders, implicit polyfills, and custom build behavior all become visible once the abstraction layer changes.
A successful migration is rarely a matter of replacing one bundler with another. It is an exercise in understanding the architecture deeply enough to preserve existing behavior while modernizing the tooling around it.
If I had to summarize the process into a single recommendation, it would be this: run Webpack and Vite side by side for as long as necessary. The ability to compare outputs, isolate regressions, and maintain a rollback path is often the difference between a smooth migration and weeks of production issues.
Top comments (0)