What is Webpack?
Webpack is a static module bundler for JavaScript applications that builds a dependency graph from your source files and outputs one or more optimized bundles for the browser.
Theory
TL;DR
- Webpack works like a factory assembly line: raw files (JS, CSS, images) enter via entry points, pass through loaders (specialized processors), and exit as browser-ready bundles
- Core difference from simple file concatenation: Webpack understands
import/requireand builds a full dependency graph instead of blindly joining files - Four concepts cover 90% of interviews: entry (where to start), output (where to write), loaders (transform non-JS files), plugins (everything else)
- Use it when you have more than 5 JS files, need CSS or image processing, or target legacy browsers
- Skip it for tiny static sites or when CDN links are enough
Quick example
// webpack.config.js - minimal working config
const path = require('path');
module.exports = {
entry: './src/index.js', // Start here
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
mode: 'production' // Enables minification + tree shaking
};
// Run: npx webpack
// Result: dist/bundle.js - one optimized file from all your modulesA few source files go in, one optimized bundle comes out. That is the whole idea.
How the dependency graph works
Webpack starts at the entry point and recursively follows every import and require it finds. Each visited file becomes a node in the graph. If index.js imports utils.js which imports helpers.js, all three end up in the bundle.
This is a different approach from a script concatenator. A concatenator blindly joins files in a given order. Webpack knows which code actually runs and which does not, so it can cut what is never used.
Entry, output, loaders, plugins
These four concepts come up in almost every interview on Webpack.
Entry is the starting file. You can define multiple entries for multi-page apps.
Output tells Webpack where to write the bundle and what to call it. path.resolve(__dirname, 'dist') is the standard pattern.
Loaders transform files Webpack cannot read natively. By default it only processes JS and JSON. Add babel-loader for ES6+ transpilation, css-loader to parse CSS imports, style-loader to inject styles into the DOM. Loaders run right-to-left inside the use array.
Plugins handle tasks loaders cannot: generating HTML files, extracting CSS into separate files, minifying output. HtmlWebpackPlugin is the most common one.
Development vs production mode
// Development: readable code, source maps, fast rebuilds
module.exports = { mode: 'development', entry: './src/index.js' };
// Production: Terser minification, tree shaking, no source maps
module.exports = { mode: 'production', entry: './src/index.js' };Forgetting mode: 'production' is the single most common mistake. A dev-mode bundle can be 10x larger than a production one. A 200KB production bundle often comes out at 2MB in dev mode.
When to use Webpack
- React or Vue app with many modules and assets: Webpack fits well, especially when you need fine-grained control over code splitting
- Legacy browser support needed: pair Webpack with Babel to transpile ES6+ down to ES5
- Complex build pipeline with custom loaders, multiple entries, or module federation: Webpack is the right tool
- Single HTML page with two script tags: skip Webpack, CDN links are faster to set up
- New project with fast iteration cycles: Vite will serve you better with its near-instant HMR
Comparison table
| Feature | Webpack | Parcel | Vite | Rollup |
|---|---|---|---|---|
| Config required | Yes (JS/JSON) | No | Minimal | Yes |
| Tree shaking | Yes | Yes | Yes | Excellent |
| HMR speed | Good | Fast | Fastest | Manual setup |
| Best for | Complex apps (React/Next.js) | Quick prototypes | Modern SPAs | Libraries |
| Learning curve | Steep | Low | Low | Medium |
How Webpack processes files internally
Webpack's compiler (built on a plugin system called Tapable) starts at the entry point, uses Acorn to parse the AST of each file, finds import/require calls, resolves paths, applies loaders, then chunks the result. In production, Terser runs minification and dead-code elimination. In dev mode, webpack-dev-server spins up an Express server with WebSocket-based HMR so the browser swaps modules without a full page reload.
Common mistakes
1. Missing mode: 'production'
// Wrong: no mode defaults to 'none', skips all optimizations
module.exports = { entry: './src/index.js' };
// Output: ~2MB unminified bundle
// Fix:
module.exports = { mode: 'production', entry: './src/index.js' };
// Output: ~200KB minified2. Wrong loader order for CSS
// Wrong: loaders run right-to-left, this breaks CSS injection into the page
{ test: /\.css$/, use: ['css-loader', 'style-loader'] }
// Fix: style-loader must be first in the array (it runs last)
{ test: /\.css$/, use: ['style-loader', 'css-loader'] }This one produces a blank page with no error message. It catches a lot of developers off guard.
3. Ignoring publicPath on subdirectory deploys
// Wrong: chunks load from root URL, 404s on /app/ path
output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') }
// Fix:
output: { publicPath: '/app/', filename: 'bundle.js', path: path.resolve(__dirname, 'dist') }4. Bundling Node-only modules for the browser
// Wrong: Webpack tries to polyfill fs, path, etc. for the browser
import fs from 'fs';
// Fix: set target and externals
module.exports = {
target: 'node',
externals: { fs: 'commonjs fs' }
};Real-world usage
- React (Create React App, Next.js): bundles JSX, CSS, and images into vendor and app chunks
- Vue CLI: processes single-file components (.vue) via vue-loader
- Angular CLI: compiles TypeScript and templates into AOT-optimized bundles
- Electron: bundles both Node and browser code for desktop apps
- Storybook: dev server with HMR for component-driven development
I have seen Webpack configs grow past 200 lines in large monorepos. At that point a Vite migration often pays off faster than expected. But for projects tied to Create React App or older setups, Webpack is still the default.
Follow-up questions
Q: What is the difference between loaders and plugins?
A: Loaders transform individual file types before they enter the bundle (Babel transpiles JS, css-loader parses CSS imports). Plugins work across the entire compilation process and handle things loaders cannot: generating HTML files, extracting CSS into separate files, defining global constants.
Q: How does tree shaking work in Webpack?
A: It relies on static analysis of ES module import/export syntax. Webpack marks unused exports as dead code, then Terser removes them during minification. For it to work, the package needs "sideEffects": false in its package.json, and you need to use ES modules rather than CommonJS.
Q: What is the difference between code splitting and dynamic imports?
A: Code splitting via optimization.splitChunks is a build-time decision: Webpack automatically separates vendor libraries from app code. Dynamic imports (import('./module.js')) are runtime-controlled: the chunk loads on demand when the browser reaches that line. Both reduce initial bundle size, but dynamic imports give you more explicit control.
Q: HMR vs live reload - what is the actual difference?
A: Live reload refreshes the whole page when a file changes. HMR (Hot Module Replacement) swaps only the changed module in the running app, preserving component state. For a React form with 10 fields already filled in, live reload wipes everything; HMR keeps the data and updates only the changed component logic.
Q: What changed from Webpack 4 to Webpack 5?
A: Three things worth knowing: (1) Webpack 5 dropped built-in Node polyfills (Buffer, process), so browser builds that relied on them need explicit configuration. (2) Chunk IDs are now deterministic by default, which improves long-term caching. (3) The Asset Modules API replaced file-loader, url-loader, and raw-loader with four built-in asset types.
Examples
Basic: bundling two files
// src/message.js
export default 'Hello Webpack';
// src/index.js
import message from './message.js';
console.log(message); // 'Hello Webpack'
// webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
mode: 'production'
};
// Run: npx webpack
// Result: dist/bundle.js (~1KB, both files merged and minified)Two source files, one output. Webpack resolved the import and merged them automatically. This is the dependency graph in its simplest form.
Intermediate: React app with CSS and HTML generation
// webpack.config.js for a real React project
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = {
entry: './src/index.js',
module: {
rules: [
{
test: /\.jsx?$/,
use: 'babel-loader',
exclude: /node_modules/ // Never transpile dependencies
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'] // Right-to-left: parse, then inject
}
]
},
plugins: [
new HtmlWebpackPlugin({ template: './public/index.html' })
],
output: {
filename: 'main.[contenthash].js', // Hash for cache busting
path: path.resolve(__dirname, 'dist')
},
mode: 'production'
};babel-loader handles JSX and ES6+. css-loader parses CSS imports. style-loader injects styles into <head> at runtime. HtmlWebpackPlugin generates dist/index.html and automatically adds the <script> tag pointing to the hashed bundle file.
Advanced: dynamic imports and code splitting
// src/index.js
const button = document.getElementById('load-btn');
button.addEventListener('click', () => {
// This import() creates a SEPARATE chunk file
// It only downloads when the button is clicked
import('./heavyComponent.js')
.then(module => module.default());
});
// webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: '[name].bundle.js',
chunkFilename: '[name].chunk.js', // Name pattern for async chunks
path: path.resolve(__dirname, 'dist')
},
mode: 'production'
};
// Output:
// dist/main.bundle.js (~1KB, loads immediately)
// dist/1.chunk.js (~50KB, loads only on button click)The initial page load stays tiny. heavyComponent.js only downloads when the user clicks the button. This is how large SPAs keep their initial load time fast. In Webpack 5, unused orphaned chunks are cleaned up automatically in production mode.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.