I cut 11 Webpack plugins down to 6 Vite plugins and dev startup dropped from 4.2s to 0.8s
vite-plugin-checker runs TypeScript and ESLint in a worker so the dev server never blocks
unplugin-icons and vite-imagetools shrank my asset pipeline by 38 percent and killed three loaders
vite-plugin-pwa plus plugin-legacy gave me offline support and old-Safari fallbacks in 14 lines of config
I spent four years tuning a Webpack config that nobody else wanted to touch. Last quarter I deleted it. The replacement is a single vite.config.ts with 6 plugins, and the dev server boots in under a second on the same machine.
The DX win that finally made me switch
The thing that broke me was waiting. My old Webpack setup booted in 4.2 seconds on a warm cache, 9 seconds cold. Hot reload took 1.8 seconds for a CSS change. I timed it because I started counting how many times a day I stared at the terminal.
Vite booted in 0.8 seconds on the same project. HMR for CSS is around 90ms. That alone would have been enough, but the real shift was vite-plugin-checker. In Webpack I had fork-ts-checker-webpack-plugin and eslint-webpack-plugin both racing the dev server, blocking compilation, and printing errors in two different formats. vite-plugin-checker runs both in a worker thread, prints a unified overlay in the browser, and never blocks the request pipeline.
// vite.config.ts
import { defineConfig } from 'vite'
import checker from 'vite-plugin-checker'
export default defineConfig({
plugins: [
checker({
typescript: true,
eslint: { lintCommand: 'eslint "./src/**/*.{ts,tsx}"' },
overlay: { initialIsOpen: false },
}),
],
})
The second DX plugin I added is unplugin-auto-import. I was tired of typing import { useState, useEffect, useMemo } from 'react' at the top of every file. With auto-import I list the libraries once and the plugin generates a .d.ts file that teaches the editor about the globals. It saved me roughly 200 lines of boilerplate across 80 components in the first repo I migrated.
The combo of these two plugins replaced four Webpack plugins (fork-ts-checker, eslint-webpack-plugin, babel-plugin-import, and a custom alias resolver I wrote in 2023). My PR review on the migration was 41 lines added, 1,103 removed. That number was the whole sales pitch.
The other thing nobody tells you is that vite-plugin-checker is honest. fork-ts-checker had a habit of caching stale type errors so you would fix a bug, save, get a green dev server, and discover the error was still there in CI. checker writes its cache per file, invalidates on save, and the worker thread is fast enough that a full project type-check finishes before I have switched back to the browser. I have not had a CI surprise from a type error in three months.
Assets without the loader graveyard
Webpack asset handling was the part of the config I was most afraid to touch. file-loader, url-loader, image-webpack-loader, svg-sprite-loader, and a custom resolver for icon sets. It worked, but it took 2.4 seconds of build time on its own, and every new icon library asked me to add another loader rule.
I replaced the whole asset pipeline with two plugins. unplugin-icons handles every icon. I drop a Phosphor or Lucide name in JSX, the plugin tree-shakes the SVG at build time, and the bundle only carries icons I actually used. My icon bundle went from 312 KB (a full sprite I shipped because nobody had time to audit) down to 11 KB.
import Icons from 'unplugin-icons/vite'
plugins: [
Icons({ compiler: 'jsx', jsx: 'react', autoInstall: true }),
]
vite-imagetools handles photography. I write import hero from './hero.jpg?w=400;800;1600&format=avif;webp;jpg&as=picture' and get back a srcset object I drop into a `` tag. No loader chain, no separate sharp config, no CI step. Build time for the assets folder dropped from 2.4 seconds to 0.7. I cover the same trick for the runtime layer in Hono: The Tiny Framework That Runs My Entire Backend, where I serve the AVIF variants directly without a CDN middle layer.
These two plugins replaced five Webpack loaders and a postbuild script. The PR description for that change was three bullets. I read the old asset config one last time, then I deleted 312 lines of it.
The bonus I did not expect: vite-imagetools query strings live in the source file, not in a config. If a designer hands me a new hero crop, I change the query string, save, and the dev server rebuilds the variants in 280ms. With Webpack I would have had to edit the loader rule, restart the dev server, and pray the cache invalidated. The colocation alone is worth the migration even if you ignore the speed numbers.
Performance and reach without a second build
A Vite setup is fast in dev, but my production target still includes a Shopify theme that gets visited from old iPads, kiosk browsers, and a surprising number of Samsung Internet users. I needed two things: a service worker for the PWA shell, and a legacy bundle for browsers that do not speak modern JS. The Vite config that ships my Shopify storefront customization does both with two plugins.
vite-plugin-pwa is the simplest service worker I have used. I tell it which routes to precache, which to network-first, and it generates the manifest, the worker, and the offline fallback. The first time I shipped it, my Lighthouse PWA score went from 47 to 100 with no other changes.
`javascript
import { VitePWA } from 'vite-plugin-pwa'
VitePWA({
registerType: 'autoUpdate',
workbox: { globPatterns: ['*/.{js,css,html,svg,woff2}'] },
manifest: { name: 'Lab', short_name: 'Lab', theme_color: '#1f1f21' },
})
`
@vitejs/plugin-legacy handles the long tail. It builds a second bundle with nomodule polyfills for browsers that do not support native ES modules. The modern bundle is 142 KB gzipped. The legacy bundle is 218 KB and only loads if a visitor needs it. I do not pay the cost in 96 percent of sessions, but I do not lose the 4 percent either.
Together these two plugins replaced workbox-webpack-plugin, babel-loader with three preset plugins, and an html-webpack-plugin template. Production build time went from 38 seconds to 11 seconds, and the modern-browser bundle is 22 percent smaller because Vite tree-shakes harder than my old Babel chain ever did. I covered why the runtime is faster too in Bun 1.2 Replaced Node in Every New RAXXO Project, and the same migration logic applies here.
One quiet detail about plugin-legacy: it ships a modulepreload polyfill for Safari 14 that I had been forgetting to load manually. After the migration, the time-to-interactive on an old iPad in the kitchen drawer dropped from 2.9s to 1.6s. I did not change a single line of app code. The plugin defaulted me into the right behavior, which is the kind of trade I will take every day of the week.
When something breaks, vite-plugin-inspect is the first stop
Six plugins is fewer than 11, but it is not zero, and plugins still fight each other. The plugin that paid for itself the first week was vite-plugin-inspect.
It mounts a /__inspect/ route in dev. I open it, pick a module, and see every transformation step in order. Which plugin touched the file. What the input was. What the output was. How long each step took. The first time I used it I found that unplugin-icons was being run twice on the same file because I had registered it before and after a custom plugin that called transform on SVGs. I deleted my custom plugin (it was a 2022 leftover), the duplicate transform vanished, and dev startup dropped another 180ms.
`javascript
import Inspect from 'vite-plugin-inspect'
plugins: [
// ...other plugins
process.env.NODE_ENV === 'development' && Inspect(),
]
`
I only enable it in dev. It adds nothing to the production bundle. If you have ever stared at a Webpack stats.json file trying to figure out which loader mangled your import, this plugin is the version of that experience that does not make you cry. It is also the only debugging tool I keep installed permanently across every Vite project I run, including the static-site repo behind my Studio page.
The second time inspect saved me was on a build that had quietly grown by 90 KB over two weeks. I opened the route, sorted modules by size, and found a date library being pulled in by three different paths because two plugins were resolving the same import differently. Fixing the resolution dropped the bundle back down in under an hour. Without inspect I would have spent a day on it. Bundle analysis tools exist as separate packages, but nothing else shows you the per-plugin transformation order, which is where most surprises actually live.
Bottom Line
I replaced 11 Webpack plugins with 6 Vite plugins. Dev startup went from 4.2s to 0.8s, production build from 38s to 11s, the icon bundle from 312 KB to 11 KB, and the config file from 1,103 lines to 41. The migration took me one weekend per repo. The hardest part was not the new tooling, it was reading the old config one last time and admitting how much of it I had been afraid to delete.
If you are still running Webpack and you have not tried Vite since the early 2.x days, the plugin ecosystem is the part that changed most. checker, auto-import, icons, imagetools, pwa, legacy, and inspect cover everything I needed. Six plugins, one config file, faster everything. That is the whole pitch.
Top comments (0)