Frontend Architecture
Part 0 of 11JavaScript Build Tooling Is a Product Decision
JavaScript Build Tooling Is a Product Decision
Webpack vs Vite vs Turbopack vs esbuild — how your build tool choice affects DX, CI time, onboarding speed, and long-term maintainability, and how to make the case for migrating to stakeholders.
The Conversation That Never Happens
Product meeting. Roadmap planning. Someone suggests a new feature. The estimates come back: "3 weeks."
What nobody mentions: 45 minutes of that daily development time is spent waiting for builds. Hot reload takes 8 seconds. CI takes 25 minutes. New developers spend their first day watching progress bars.
Build tooling isn't a technical detail. It's a tax on every feature you ship.
THE HIDDEN COST
────────────────────────────────────────────────────────────────────
Old Build Tool Modern Build Tool
────────────── ─────────────────
Dev server start 45 seconds 800 milliseconds
Hot reload 8 seconds <100 milliseconds
Production build 12 minutes 90 seconds
CI pipeline 25 minutes 8 minutes
Developer hours/week waiting: 4.5 hours 0.5 hours
Annual cost (10 devs, $150k avg):
Waiting time alone: $175,000/year $19,500/year
That's before counting context switching, frustration,
and the features that never got built.
This post is about treating build tooling as the product decision it is.
The Landscape
┌─────────────────────────────────────────────────────────────────┐
│ BUILD TOOL EVOLUTION │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 2014-2020: Webpack Era │
│ ├── Everything is configurable │
│ ├── Plugin ecosystem for every need │
│ ├── Complex, slow, but complete │
│ └── Still powers most production apps │
│ │
│ 2020-Present: Native Speed Era │
│ ├── esbuild (Go) - 10-100x faster bundling │
│ ├── Vite - Dev server that doesn't bundle in dev │
│ ├── Turbopack (Rust) - Vercel's webpack successor │
│ ├── swc (Rust) - Fast TypeScript/JSX transform │
│ └── Bun - JavaScript runtime with native bundler │
│ │
│ The shift: JavaScript tools → Native (Rust/Go) tools │
│ │
└─────────────────────────────────────────────────────────────────┘
The Tools Compared
┌──────────────────────────────────────────────────────────────────────────────┐
│ TOOL COMPARISON │
├─────────────┬────────────────┬─────────────┬─────────────┬──────────────────┤
│ │ Webpack │ Vite │ Turbopack │ esbuild │
├─────────────┼────────────────┼─────────────┼─────────────┼──────────────────┤
│ Language │ JavaScript │ JS + esbuild│ Rust │ Go │
│ Dev speed │ Slow │ Very fast │ Very fast │ Extremely fast │
│ Build speed │ Slow │ Fast │ Fast │ Extremely fast │
│ Config │ Complex │ Simple │ Minimal │ Minimal │
│ Plugins │ Thousands │ Hundreds │ Limited │ Basic │
│ Maturity │ Very mature │ Mature │ Beta │ Mature │
│ Framework │ Any │ Any │ Next.js │ Any │
│ Production │ Proven │ Proven │ Early │ Proven │
├─────────────┴────────────────┴─────────────┴─────────────┴──────────────────┤
│ │
│ TYPICAL DEV SERVER START TIME (large app, ~2000 modules) │
│ ───────────────────────────────────────────────────────── │
│ Webpack: 30-60 seconds │
│ Vite: 300-800 milliseconds │
│ Turbopack: 200-500 milliseconds │
│ esbuild: 100-300 milliseconds │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
Webpack: The Incumbent
Webpack isn't slow because it's bad. It's slow because it does everything, and it does it in JavaScript.
Why Teams Still Use Webpack
// webpack.config.js - The kitchen sink
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const WorkboxPlugin = require('workbox-webpack-plugin');
const SentryWebpackPlugin = require('@sentry/webpack-plugin');
module.exports = {
entry: './src/index.tsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].chunk.js',
publicPath: '/',
clean: true,
},
module: {
rules: [
{
test: /\.tsx?$/,
use: [
{
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-typescript',
],
plugins: [
'@babel/plugin-transform-runtime',
'babel-plugin-styled-components',
],
},
},
],
exclude: /node_modules/,
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
],
},
{
test: /\.svg$/,
use: ['@svgr/webpack', 'url-loader'],
},
{
test: /\.(png|jpg|gif|woff|woff2)$/,
type: 'asset',
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
new CopyWebpackPlugin({
patterns: [{ from: 'public', to: '.' }],
}),
process.env.ANALYZE && new BundleAnalyzerPlugin(),
new WorkboxPlugin.GenerateSW({
clientsClaim: true,
skipWaiting: true,
}),
new SentryWebpackPlugin({
include: './dist',
ignore: ['node_modules'],
}),
].filter(Boolean),
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
runtimeChunk: 'single',
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
devServer: {
hot: true,
historyApiFallback: true,
port: 3000,
},
};
The Webpack Tax
WHAT HAPPENS ON `npm run dev`:
────────────────────────────────────────────────────────────────────
1. Parse webpack config (JavaScript)
2. Resolve all entry points
3. For EVERY file:
├── Read file from disk
├── Run through loader chain (babel-loader, etc.)
├── Parse AST
├── Transform code
├── Resolve imports
└── Add to dependency graph
4. Build dependency graph for entire app
5. Bundle everything together
6. Generate source maps
7. Write to memory/disk
8. Start dev server
Time: 30-60 seconds for large apps
Then, on EVERY file change:
1. Detect change
2. Invalidate module cache
3. Re-run loaders for changed file
4. Rebuild affected chunks
5. Update via HMR
Time: 2-10 seconds
Vite: The Paradigm Shift
Vite doesn't try to be a faster bundler. It avoids bundling in development entirely.
How Vite Works
┌─────────────────────────────────────────────────────────────────┐
│ VITE ARCHITECTURE │
├─────────────────────────────────────────────────────────────────┤
│ │
│ DEVELOPMENT (no bundling!) │
│ ───────────────────────── │
│ │
│ Browser requests: /src/App.tsx │
│ │ │
│ ▼ │
│ Vite server intercepts │
│ │ │
│ ▼ │
│ Transform on demand (esbuild): │
│ ├── TypeScript → JavaScript │
│ ├── JSX → JavaScript │
│ ├── Rewrite imports to URLs │
│ └── Return as ES module │
│ │ │
│ ▼ │
│ Browser executes native ES modules │
│ │
│ Only transforms files as they're requested! │
│ │
│ PRODUCTION (uses Rollup) │
│ ──────────────────────── │
│ │
│ Full bundle with tree shaking, code splitting, minification │
│ │
└─────────────────────────────────────────────────────────────────┘
Vite Configuration
// vite.config.ts - Dramatically simpler
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [
react(),
tsconfigPaths(),
],
server: {
port: 3000,
},
build: {
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
},
},
},
},
});
// That's it. No loaders. No complex rules.
// Vite handles TypeScript, JSX, CSS, assets out of the box.
The Speed Difference Explained
WHY VITE IS FASTER IN DEV:
────────────────────────────────────────────────────────────────────
Webpack approach:
┌─────────────────────────────────────────────────────────────────┐
│ │
│ App has 2000 modules │
│ │ │
│ ▼ │
│ Bundle ALL 2000 modules before serving │
│ │ │
│ ▼ │
│ Serve bundled JavaScript │
│ │
│ Time to first paint: ~40 seconds │
│ │
└─────────────────────────────────────────────────────────────────┘
Vite approach:
┌─────────────────────────────────────────────────────────────────┐
│ │
│ App has 2000 modules │
│ │ │
│ ▼ │
│ Pre-bundle node_modules ONCE (esbuild, <1 sec) │
│ │ │
│ ▼ │
│ Serve index.html immediately │
│ │ │
│ ▼ │
│ Browser requests modules as needed │
│ Transform on-demand (~5ms per module) │
│ │
│ Time to first paint: ~800ms │
│ (Most modules loaded lazily) │
│ │
└─────────────────────────────────────────────────────────────────┘
Turbopack: The Next.js Future
Turbopack is Vercel's Rust-based successor to Webpack, designed specifically for Next.js.
Current State
// next.config.js - Enabling Turbopack
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
turbo: {
rules: {
// Custom loaders (limited support)
'*.svg': {
loaders: ['@svgr/webpack'],
as: '*.js',
},
},
},
},
};
module.exports = nextConfig;
// Run with: next dev --turbo
Turbopack's Approach
TURBOPACK ARCHITECTURE:
────────────────────────────────────────────────────────────────────
1. INCREMENTAL COMPUTATION
├── Only recompute what changed
├── Cache at function level, not file level
└── Never duplicate work
2. LAZY BUNDLING
├── Don't bundle until needed
├── Bundle per-route, not whole app
└── Stream results as ready
3. NATIVE SPEED
├── Written in Rust (Turbo Engine)
├── 10x faster than Webpack
└── 4x faster than Vite (in benchmarks)
CAVEATS (as of 2024):
├── Still in beta for Next.js
├── Limited plugin ecosystem
├── Some Webpack features not yet supported
├── Production builds still use Webpack
└── Only works with Next.js
esbuild: The Foundation
esbuild isn't typically used directly — it powers other tools (Vite, tsup, etc.).
Why esbuild Is So Fast
ESBUILD BENCHMARKS (bundling three.js):
────────────────────────────────────────────────────────────────────
esbuild: 0.33 seconds
Parcel 2: 7.8 seconds
Rollup + Terser: 34 seconds
Webpack 5: 41 seconds
That's 10-100x faster.
WHY:
├── Written in Go (compiled, parallel)
├── Parallel parsing and code generation
├── Minimal AST passes
├── Avoids expensive operations
└── No JavaScript overhead
Direct esbuild Usage
// build.js - Direct esbuild for simple projects
import * as esbuild from 'esbuild';
// Development
const ctx = await esbuild.context({
entryPoints: ['src/index.tsx'],
bundle: true,
outdir: 'dist',
sourcemap: true,
format: 'esm',
target: 'es2020',
loader: {
'.png': 'file',
'.svg': 'file',
},
define: {
'process.env.NODE_ENV': '"development"',
},
});
await ctx.watch();
await ctx.serve({ port: 3000 });
// Production
await esbuild.build({
entryPoints: ['src/index.tsx'],
bundle: true,
outdir: 'dist',
minify: true,
sourcemap: true,
format: 'esm',
splitting: true,
metafile: true,
});
esbuild Limitations
WHAT ESBUILD DOESN'T DO:
────────────────────────────────────────────────────────────────────
✗ Type checking (by design - use tsc separately)
✗ Complex code transformations
✗ Polyfilling (needs separate tool)
✗ HMR (basic only)
✗ HTML generation
✗ CSS preprocessing (Sass, Less)
SOLUTION: Use esbuild as a component
├── Vite uses esbuild for deps + transforms
├── tsup uses esbuild for library builds
├── tsx uses esbuild for running TypeScript
└── Combine with other tools for full solution
The Impact Matrix
Developer Experience
┌─────────────────────────────────────────────────────────────────┐
│ DEVELOPER EXPERIENCE IMPACT │
├─────────────────────────────────────────────────────────────────┤
│ │
│ FLOW STATE │
│ ────────── │
│ Webpack: Change → Wait 5s → See result → Forgot what testing │
│ Vite: Change → 50ms → See result → Still in context │
│ │
│ ONBOARDING │
│ ────────── │
│ Webpack: "Run npm install, then wait... now npm start, wait..."│
│ Vite: "Run npm install, npm run dev" → Working in 30 sec │
│ │
│ DEBUGGING │
│ ───────── │
│ Webpack: "Is this a config issue or code issue?" │
│ Vite: Config is 20 lines. It's probably your code. │
│ │
│ MAINTENANCE │
│ ─────────── │
│ Webpack: Upgrade breaks 3 loaders, debug for a day │
│ Vite: Upgrade usually just works │
│ │
└─────────────────────────────────────────────────────────────────┘
CI/CD Impact
┌─────────────────────────────────────────────────────────────────┐
│ CI TIME COMPARISON (real project) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ WEBPACK CI PIPELINE │
│ ├── Checkout: 30s │
│ ├── Install deps: 2m │
│ ├── Lint: 45s │
│ ├── Type check: 1m │
│ ├── Unit tests: 2m │
│ ├── Build: 12m ← THE PROBLEM │
│ ├── E2E tests: 5m │
│ └── Deploy: 30s │
│ TOTAL: 24 minutes │
│ │
│ VITE CI PIPELINE │
│ ├── Checkout: 30s │
│ ├── Install deps: 2m (cacheable) │
│ ├── Lint: 45s │
│ ├── Type check: 1m │
│ ├── Unit tests: 2m │
│ ├── Build: 90s ← 8x FASTER │
│ ├── E2E tests: 5m │
│ └── Deploy: 30s │
│ TOTAL: 13 minutes │
│ │
│ MONTHLY CI COST (100 builds/day, $0.008/minute): │
│ Webpack: $576/month │
│ Vite: $312/month │
│ │
└─────────────────────────────────────────────────────────────────┘
Team Productivity
REAL MEASUREMENTS (before/after Vite migration):
────────────────────────────────────────────────────────────────────
Team: 8 developers
Project: React SPA, ~1500 components
Before (Webpack) After (Vite)
──────────────── ────────────
Dev server start 48 seconds 1.2 seconds
HMR update 4.8 seconds 120ms
Production build 14 minutes 2.1 minutes
Daily time waiting/dev 52 minutes 8 minutes
Weekly productivity gain: 5.5 hours/developer
Monthly gain: 176 developer-hours
Qualitative improvements:
├── Developers actually run the app while coding (was avoided)
├── PR feedback cycle shortened
├── Less context switching
├── Better code quality (easier to test changes)
└── Improved morale (seriously)
Migration Strategy
Assessment Phase
// scripts/analyze-build.ts
import { execSync } from 'child_process';
import { performance } from 'perf_hooks';
interface BuildAnalysis {
devStartTime: number;
hmrTime: number;
productionBuildTime: number;
bundleSize: number;
moduleCount: number;
webpackPlugins: string[];
customLoaders: string[];
complexConfigs: string[];
}
function analyzeBuild(): BuildAnalysis {
// Measure dev start
const devStart = performance.now();
execSync('npm run dev &', { timeout: 120000 });
// ... wait for ready signal
const devStartTime = performance.now() - devStart;
// Count webpack complexity
const config = require('../webpack.config.js');
const plugins = config.plugins?.map((p: any) => p.constructor.name) || [];
const loaders = extractLoaders(config);
return {
devStartTime,
hmrTime: measureHMR(),
productionBuildTime: measureBuild(),
bundleSize: getBundleSize(),
moduleCount: getModuleCount(),
webpackPlugins: plugins,
customLoaders: loaders,
complexConfigs: identifyComplexConfigs(config),
};
}
// Output:
// {
// devStartTime: 48000,
// hmrTime: 4800,
// productionBuildTime: 840000,
// bundleSize: 2400000,
// moduleCount: 1847,
// webpackPlugins: ['HtmlWebpackPlugin', 'MiniCssExtractPlugin', ...],
// customLoaders: ['svg-custom-loader'],
// complexConfigs: ['Module Federation', 'Custom resolve']
// }
Migration Checklist
## Pre-Migration
- [ ] Document current webpack config
- [ ] List all plugins and their purposes
- [ ] Identify custom loaders
- [ ] Audit environment variable usage
- [ ] Check for webpack-specific imports (require.context, etc.)
- [ ] Measure current build times (baseline)
- [ ] Identify blocking dependencies (not ESM compatible)
## Vite Migration
- [ ] Install Vite and plugins
- [ ] Create vite.config.ts
- [ ] Update index.html (Vite serves from root)
- [ ] Replace process.env with import.meta.env
- [ ] Update path aliases
- [ ] Handle CSS modules differences
- [ ] Replace require() with import
- [ ] Update test configuration (Vitest recommended)
- [ ] Update CI/CD scripts
## Post-Migration
- [ ] Verify all features work
- [ ] Measure new build times
- [ ] Update documentation
- [ ] Train team on differences
- [ ] Remove webpack dependencies
Step-by-Step Migration
// Step 1: Add Vite alongside Webpack
// package.json
{
"scripts": {
"dev": "webpack serve", // Keep existing
"dev:vite": "vite", // Add Vite
"build": "webpack", // Keep existing
"build:vite": "vite build" // Add Vite
}
}
// Step 2: Create Vite config
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [react(), tsconfigPaths()],
// Handle environment variables
define: {
'process.env': process.env, // Compatibility shim
},
// Match webpack's public path behavior
base: '/',
// Handle assets like webpack
assetsInclude: ['**/*.svg', '**/*.png'],
resolve: {
alias: {
// Match webpack aliases
'@': '/src',
},
},
});
// Step 3: Update index.html
// Before (public/index.html - webpack):
// <div id="root"></div>
// After (index.html in root - Vite):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
// Step 4: Environment variables
// Before (webpack):
// process.env.REACT_APP_API_URL
// After (Vite):
// import.meta.env.VITE_API_URL
// Create compatibility layer if needed:
// src/config.ts
export const config = {
apiUrl: import.meta.env.VITE_API_URL || process.env.REACT_APP_API_URL,
};
// Step 5: Run both, compare, switch
Handling Common Migration Issues
// Issue 1: require() calls
// Before:
const image = require('./logo.png');
// After:
import image from './logo.png';
// Or dynamic:
const image = new URL('./logo.png', import.meta.url).href;
// Issue 2: require.context (webpack-specific)
// Before:
const modules = require.context('./modules', true, /\.tsx$/);
// After:
const modules = import.meta.glob('./modules/**/*.tsx');
// Returns: { './modules/a.tsx': () => import('./modules/a.tsx') }
// Issue 3: CSS Modules naming
// Before (webpack): styles.myClass
// After (Vite): Same, but file must be named *.module.css
// Issue 4: SVG imports
// Before (with @svgr/webpack):
import { ReactComponent as Logo } from './logo.svg';
// After (with vite-plugin-svgr):
import Logo from './logo.svg?react';
// Issue 5: Path aliases
// Vite uses tsconfig.json paths OR vite.config.ts resolve.alias
// Make sure both are aligned
// Issue 6: Global SCSS variables
// Before (webpack): Injected via sass-loader additionalData
// After (Vite):
// vite.config.ts
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/variables.scss";`,
},
},
},
Making the Case to Stakeholders
The Executive Summary
PROPOSAL: Migrate Build System from Webpack to Vite
────────────────────────────────────────────────────────────────────
PROBLEM
Every developer loses 45-60 minutes daily to slow builds.
That's 4 hours/week × 10 developers = 40 hours/week of lost productivity.
SOLUTION
Migrate to Vite, a modern build tool that's 10-50x faster.
INVESTMENT
├── Migration effort: 2-3 weeks (1-2 developers)
├── Testing & validation: 1 week
├── Risk: Low (can run parallel, rollback easy)
└── Total: ~3-4 weeks
RETURN
├── Daily time saved per developer: 45 minutes
├── Weekly team productivity gain: 37.5 hours
├── Annual hours recovered: 1,950 hours
├── At $75/hour fully loaded: $146,000/year value
├── CI cost reduction: ~$3,000/year
└── ROI: Pays for itself in < 2 months
ADDITIONAL BENEFITS
├── Faster onboarding (new devs productive sooner)
├── Better developer morale (less frustration)
├── Faster PR cycles (quicker feedback)
└── Future-proof (industry moving this direction)
The Technical Brief
## Technical Migration Plan: Webpack → Vite
### Current State
- Build tool: Webpack 5
- Dev server start: 48 seconds
- Hot reload: 4.8 seconds
- Production build: 14 minutes
- Configuration: 400 lines across 3 files
### Proposed State
- Build tool: Vite 5
- Dev server start: <2 seconds (projected)
- Hot reload: <200ms (projected)
- Production build: <3 minutes (projected)
- Configuration: ~50 lines
### Migration Approach
1. Run Vite parallel to Webpack (week 1)
2. Migrate features incrementally (week 1-2)
3. Team testing and feedback (week 2)
4. CI/CD integration (week 3)
5. Full cutover (week 3)
6. Remove Webpack (week 4)
### Risk Mitigation
- Parallel operation allows instant rollback
- Feature flags for gradual adoption
- Comprehensive test suite validates parity
- Webpack config preserved until stable
### Success Metrics
- Dev server start < 2 seconds
- HMR updates < 200ms
- Production build < 3 minutes
- Zero regression in functionality
- Positive developer feedback
Addressing Objections
OBJECTION: "If it ain't broke, don't fix it"
────────────────────────────────────────────────────────────────────
RESPONSE: It is broke. Every developer loses an hour daily to builds.
We've just normalized the pain. That's 10,000+ hours/year of
productivity we're leaving on the table.
OBJECTION: "What if the migration introduces bugs?"
────────────────────────────────────────────────────────────────────
RESPONSE: We'll run both systems in parallel during migration. Our
existing test suite catches regressions. We can roll back instantly
if needed. The migration changes build tooling, not application code.
OBJECTION: "Our webpack config is complex for a reason"
────────────────────────────────────────────────────────────────────
RESPONSE: Most of that complexity addresses problems Vite solves
differently or doesn't have. We've audited our config - 70% is
unnecessary with Vite. The remaining 30% has Vite equivalents.
OBJECTION: "We don't have bandwidth for infrastructure work"
────────────────────────────────────────────────────────────────────
RESPONSE: This IS product work. Every feature ships faster after
migration. A 3-week investment returns 40 hours/week permanently.
What feature delivers that ROI?
OBJECTION: "Let's wait until Turbopack is stable"
────────────────────────────────────────────────────────────────────
RESPONSE: Turbopack is Next.js only and still beta. Vite is proven,
stable, and can migrate to Turbopack later if needed. The cost of
waiting is 40 hours/week × however long we wait.
Decision Framework
┌─────────────────────────────────────────────────────────────────┐
│ WHICH TOOL TO CHOOSE │
├─────────────────────────────────────────────────────────────────┤
│ │
│ START HERE: What framework are you using? │
│ │ │
│ ├── Next.js │
│ │ └── Use what Next.js provides │
│ │ ├── Currently: Webpack (with Turbopack opt-in) │
│ │ └── Try --turbo for dev, evaluate stability │
│ │ │
│ ├── React (CRA/custom) │
│ │ └── Migrate to Vite │
│ │ ├── Mature, fast, excellent DX │
│ │ └── Use @vitejs/plugin-react │
│ │ │
│ ├── Vue │
│ │ └── Use Vite (it was built for Vue) │
│ │ │
│ ├── Library development │
│ │ └── Use tsup (esbuild-based) │
│ │ ├── Simple config │
│ │ ├── ESM + CJS output │
│ │ └── TypeScript declarations │
│ │ │
│ └── Custom/complex requirements │
│ ├── Many custom loaders? → Stay on Webpack │
│ ├── Module Federation? → Webpack or Vite + plugin │
│ └── Simple app? → Vite │
│ │
└─────────────────────────────────────────────────────────────────┘
When to Stay on Webpack
VALID REASONS TO STAY ON WEBPACK:
────────────────────────────────────────────────────────────────────
✓ Heavy Module Federation usage
└── Vite support exists but less mature
✓ Complex custom loaders without Vite equivalents
└── Audit first - most have alternatives
✓ Build working perfectly, team bandwidth critical
└── But calculate opportunity cost
✓ Legacy browser requirements (IE11)
└── Vite targets modern browsers by default
✓ Specific webpack plugin with no alternative
└── List is shrinking rapidly
NOT VALID REASONS:
────────────────────────────────────────────────────────────────────
✗ "We've always used webpack"
✗ "It works" (but slowly)
✗ "Migration seems risky" (it's low risk)
✗ "Team would need to learn new tool" (minimal learning)
✗ "Vite is too new" (it's 4+ years old, battle-tested)
Quick Reference
Tool Selection
SIMPLE SPA/APP: Vite
NEXT.JS: Next.js default (try --turbo)
LIBRARY: tsup (esbuild)
MONOREPO: Turborepo + Vite
NEED MAX COMPATIBILITY: Webpack 5
NEED MAX SPEED: esbuild (direct)
Config Comparison
// WEBPACK: Complex
module.exports = {
entry: './src/index.tsx',
output: { /* ... */ },
module: {
rules: [
{ test: /\.tsx?$/, use: 'ts-loader' },
{ test: /\.css$/, use: ['style-loader', 'css-loader'] },
// ... more rules
],
},
plugins: [ /* many plugins */ ],
resolve: { /* aliases, extensions */ },
devServer: { /* options */ },
optimization: { /* splitting, minimizing */ },
};
// VITE: Simple
export default defineConfig({
plugins: [react()],
});
// That's it. TypeScript, CSS, JSX work out of the box.
Migration Time Estimates
PROJECT SIZE MIGRATION TIME RISK
─────────────────────────────────────────────────
Small (<50 components) 1-2 days Low
Medium (50-200) 1 week Low
Large (200-1000) 2-3 weeks Medium
Enterprise (1000+) 4-6 weeks Medium
Closing Thoughts
Build tooling isn't a technical choice hidden in devDependencies. It's infrastructure that affects every developer, every day, on every feature.
The JavaScript ecosystem has made a generational leap in build performance. Native-speed tools like esbuild and Vite aren't marginal improvements — they're 10-100x faster. That's the difference between a build that interrupts your thought process and one you don't notice.
If your team is still on Webpack and waiting for builds, run the numbers. Calculate the hours lost. Present the ROI. The migration pays for itself in weeks, and the return continues indefinitely.
The best time to migrate was when Vite hit 1.0. The second best time is now.
What did you think?