Bundlers and Build Tools
Understanding Webpack, Vite, Turbopack, and build optimization
Bundlers transform your dev-friendly code into production-optimized bundles.
What you'll learn
Understand Webpack core concepts: entry, output, loaders, plugins
Compare Vite's ESM dev server vs traditional bundler approach
Use dynamic imports for code splitting
Explain tree shaking and dead code elimination
Configure environment variables for different modes
Analyze and optimize bundle size
Webpack Core Concepts
// webpack.config.js — classic setup
module.exports = {
entry: "./src/index.tsx",
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].[contenthash].js",
},
module: {
rules: [
{ test: /\.tsx?$/, use: "ts-loader" }, // loader
{ test: /\.css$/, use: ["style-loader", "css-loader"] },
],
},
plugins: [new HtmlWebpackPlugin({ template: "./public/index.html" })],
}
| Concept | Role |
|---|---|
| Entry | Where bundler starts resolving deps |
| Output | Where final bundles go |
| Loaders | Transform files (TS → JS, SCSS → CSS) |
| Plugins | Extend behavior (HTML generation, minification) |
| Chunks | Split output files |
Vite Approach
Vite serves ESM in dev — no bundling needed until production:
// vite.config.ts
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ["react", "react-dom"],
},
},
},
},
})
HMR — Hot Module Replacement
Applies code changes without full reload. Preserves state.
Webpack: HMR via WebSocket, rebuilds changed modules
Vite: HMR via native ESM — only invalidate browser cache for changed file
No rebuild needed — cold start ~50ms vs Webpack ~2-5s
Turbopack: Rust-based incremental engine
Only recomputes changed cells — < 50ms updates at any scale
Code Splitting
// Static import — always bundled
import { HeavyChart } from "./HeavyChart"
// Dynamic import — separate chunk
import("./HeavyChart").then(({ HeavyChart }) => {
// Loaded on demand
})
// React.lazy (builds on dynamic import)
const HeavyChart = lazy(() => import("./HeavyChart"))
// Next.js dynamic
import dynamic from "next/dynamic"
const HeavyComponent = dynamic(() => import("./Heavy"), {
loading: () => <Skeleton />,
ssr: false, // Skip server render for client-only components
})
Tree Shaking
Dead code elimination relies on ES module static analysis:
// utils.ts
export function usedFn() {
return 42
}
export function deadFn() {
return "never called"
} // Removed in build
// app.ts — only imports usedFn
import { usedFn } from "./utils"
Tree shaking works when:
- ES module syntax (
import/export), not CommonJS (require) sideEffects: falseinpackage.json- No dynamic imports that defeat static analysis
Gotcha: Side Effects
If a module modifies global state (e.g., polyfill), set "sideEffects": ["./polyfill.js"] in package.json — otherwise tree shaker might remove it
thinking it's unused.
Environment Variables
// Vite
console.log(import.meta.env.VITE_API_URL)
console.log(import.meta.env.DEV) // true in dev
console.log(import.meta.env.PROD) // true in production
// Webpack (via DefinePlugin or dotenv-webpack)
console.log(process.env.API_URL)
// Next.js
console.log(process.env.NEXT_PUBLIC_API_URL)
// Prefix with NEXT_PUBLIC_ for client-side access
Turbopack (Next.js)
Next.js uses Turbopack by default in dev (v15+):
# Enabled automatically in Next.js 15+
next dev --turbo
Benefits vs Webpack:
- Rust-based — 10-100x faster for large apps
- Incremental computation — only rebuilds changed cells
- Caching at function level, not file level
Challenge
Identify optimization opportunities in this bundle config.
Bundle Optimization
Code
ts
1// Current webpack.config.js — find 3+ optimization issues2const path = require("path")3 4module.exports = {5entry: "./src/index.tsx",6output: {7path: path.resolve(__dirname, "dist"),8filename: "bundle.js",9},10module: {11rules: [12{ test: /\.tsx?$/, use: "ts-loader" },13 { test: /\.css$/, use: ["style-loader", "css-loader"] },14],15},16resolve: {17extensions: [".ts", ".tsx", ".js"],18},19// Issues to fix:20// 1. No content hash in filename (cache busting)21// 2. No optimization.splitChunks (vendor bundle)22// 3. No mode set (no minification)23// 4. No HtmlWebpackPlugin24// Bonus: Add bundle analyzer25}Key Takeaways
Webpack: entry, loaders, plugins, output — full control, more config
Vite: native ESM in dev (fast), Rollup for production (efficient)
Dynamic imports create separate chunks loaded on demand
Tree shaking removes dead ES module exports
Turbopack: Rust-based incremental engine in Next.js
Analyze bundles with webpack-bundle-analyzer or vite-bundle-visualizer
Use contenthash for cache busting, splitChunks for vendor separation