I’ve been using webpack for my Vue front-end apps for a few years. It works, but the config is the most tedious file in the project. I just migrated my first app to Vite. It was easy. Here’s what happened.
The migration
My app is a Vue 3 SPA with Tailwind CSS, PostCSS, and shell scripts for the build workflow. Everything runs in Docker. I was using webpack 5 with vue-loader, HtmlWebpackPlugin, MiniCssExtractPlugin, CopyPlugin, and Terser.
The Vite config ended up much smaller. Vite handles SFC compilation, CSS extraction, HTML generation, and asset copying without plugins. One thing that carries over is the vue alias for the full build with template compiler:
resolve: {
alias: {
vue: 'vue/dist/vue.esm-bundler.js',
},
},
Without this, Vue throws an error about runtime compilation not being supported. It’s the same alias webpack needed.
Three file moves:
html/index.htmlgoes to the project root with a<script type="module">tag addedstatic/becomespublic/(Vite serves this directory at root automatically)- The manual
vendor.jsentry file goes away (Vite splits vendor code viamanualChunks)
The PostCSS config needed a change from string shorthand plugin names ('tailwindcss') to explicit imports (import tailwindcss from 'tailwindcss'). Vite requires this. The old format causes an “Invalid PostCSS Plugin” error at build time.
The Tailwind content paths needed updating since html/ no longer exists. './html/**/*.html' becomes './index.html'.
package.json needs "type": "module" for the ESM configs. Fourteen webpack-related dependencies come out. Two come in: vite and @vitejs/plugin-vue.
The build script changes from npx webpack to npx vite build. The dev server changes from npx webpack serve to npx vite.
HMR
I had HMR disabled with webpack (hot: false) because I couldn’t get the WebSocket working through my Caddy reverse proxy. With Vite it works. The key was clientPort instead of port in the server.hmr config:
server: {
hmr: {
protocol: 'wss',
host: 'mysite.localhost',
clientPort: 8443,
},
},
port makes Vite create a separate WebSocket server on that port, which fails inside Docker where only port 3000 is mapped. clientPort only changes the URL the browser connects to. The HMR server stays on 3000, Caddy proxies the WebSocket through to it.
Docker also needs server.watch.usePolling: true for file watching across the volume mount boundary. The old WATCHPACK_POLLING=true env var was webpack-specific.
In Caddy, the /ws handler for the webpack dev server WebSocket is no longer needed. Before, I had a separate handler for it. After, Vite’s HMR WebSocket passes through the main proxy with no special routing.
Production build differences
Vite outputs ESM modules (<script type="module">) instead of IIFE scripts. Modern browsers handle this fine. The vendor/app split is still there via manualChunks. Content hashing is automatic. esbuild replaces Terser as the minifier, which is much faster. Source maps are off by default; add build.sourcemap: true if you need them.
Webpack produced 334 KB of JS+CSS on disk. Vite produces 330 KB. Both gzip to about 97 KB over the wire. Comparable output.
What I ran into
The PostCSS string shorthand issue was the first thing that broke. The HMR clientPort vs port issue was the second. The vue alias for the full build is easy to forget, but you get a clear error from Vue and the fix is the same alias webpack needed.
Nothing else broke. No component changes. No router changes. No changes to the app code at all. The migration is purely a build tool change.
Worth it
Vite’s dev server starts instantly instead of waiting on a webpack compile. HMR works through my proxy. The config is much smaller. The dependency list dropped by more than half. I’ll be migrating my other apps the same way.