What is Webpack?
Webpack is a module bundler. Starting from one or more entry points, it follows every import and builds a graph of your application’s modules and assets. It then transforms and combines them into a small number of bundles the browser can load efficiently.
Released in 2012, Webpack became the backbone of the modern front-end build. Vite is now the default for new projects because it is faster and simpler, but Webpack still powers an enormous number of production applications, and its concepts — the dependency graph, loaders, plugins and code splitting — are the vocabulary of every bundler that followed.
Entry and output
The two required pieces of configuration are where the graph starts and where the bundles go.
// webpack.config.js
import path from "node:path";
export default {
entry: "./src/index.js",
output: {
path: path.resolve(import.meta.dirname, "dist"),
filename: "[name].[contenthash].js",
clean: true,
},
};
entry is the starting module. output.filename uses a content hash so browsers can cache bundles forever and only re-download changed ones. clean: true removes stale files before each build.
Loaders
Webpack only understands JavaScript by default. Loaders transform other files into modules as they enter the graph.
// loaders.js
export default {
module: {
rules: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: "ts-loader",
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
{
test: /\.(png|svg|woff2)$/,
type: "asset",
},
],
},
};
A rule has a test for which files it matches and a use for the loader or loaders to apply. Loaders run right to left, so ["style-loader", "css-loader"] first parses the CSS and then injects it into the page. Webpack 5 also has asset modules, which replace the old file-loader and url-loader for images and fonts.
Plugins
Plugins extend the build itself. Where a loader transforms a file, a plugin hooks into the compilation lifecycle.
// plugins.js
import HtmlWebpackPlugin from "html-webpack-plugin";
import { DefinePlugin } from "webpack";
export default {
plugins: [
new HtmlWebpackPlugin({ template: "./src/index.html" }),
new DefinePlugin({
"process.env.NODE_ENV": JSON.stringify("production"),
}),
],
};
Common plugins generate the HTML file, define environment variables, copy static assets, analyse bundle size and clean the output. This is where most of Webpack’s power — and most of its complexity — lives.
Resolve and aliases
The resolve options control how imports are found.
// resolve.js
export default {
resolve: {
extensions: [".ts", ".tsx", ".js"],
alias: {
"@": path.resolve(import.meta.dirname, "src"),
},
},
};
extensions lets you import without specifying a file extension, and alias creates shortcuts so imports are not littered with ../../.. paths. The same alias is usually mirrored in the TypeScript config so the editor agrees with the bundler.
Code splitting and tree shaking
Two optimisations dominate production performance.
Code splitting breaks the bundle into chunks loaded on demand. Dynamic import() is the primary tool, and splitChunks automatically extracts shared dependencies.
// split.js
export default {
optimization: {
splitChunks: { chunks: "all" },
},
};
Tree shaking removes unused exports, but only when it can analyse the imports statically. That means ES modules (import/export) rather than CommonJS, and modules that are free of side effects. Marking packages as sideEffects: false in their package.json lets Webpack prune aggressively.
The development server
webpack-dev-server serves the build in development with hot module replacement.
// devServer.js
export default {
devServer: {
port: 3000,
hot: true,
historyApiFallback: true,
proxy: [{ context: ["/api"], target: "http://localhost:8787" }],
},
};
historyApiFallback makes client-side routing work by serving index.html for unknown paths, and proxy forwards API requests to your backend. The dev server keeps the build in memory, so rebuilds are fast.
When to use Webpack
Webpack is still a strong choice when you maintain an existing application, depend on a specific loader or plugin, or need advanced features such as module federation for micro-frontends. Its configuration is verbose, but it is also extremely capable and predictable once understood.
For new projects, Vite is usually the better default: faster startup, simpler configuration and a modern plugin API. Learning Webpack is still worthwhile because its concepts transfer directly — Vite’s plugin interface is Rollup-compatible and the same ideas of graph, transforms and splitting apply everywhere.
Best practices
- Keep configuration split into
common,devandprodfiles or functions. - Use content hashes in filenames for long-term caching.
- Prefer ES modules so tree shaking can work.
- Split heavy features with dynamic imports.
- Mark side-effect-free packages so unused code is removed.
- Mirror
resolve.aliasin the TypeScript config. - Analyse the bundle with a visualiser when it grows unexpectedly.
Common mistakes
- Shipping everything in one bundle and never code splitting.
- Using CommonJS and wondering why tree shaking does nothing.
- Putting a loader in the wrong order and getting confusing errors.
- Duplicating configuration across environments instead of composing it.
- Aliasing in Webpack but not in TypeScript, so the editor disagrees.
- Adding a plugin that duplicates a built-in Webpack 5 feature.
Where to go next
Webpack is the historical foundation of modern build tooling and still a dependable choice for existing applications. Compare it with Vite for new projects, understand the npm packages that supply loaders and plugins, and see how the same ideas appear in Turborepo for monorepo builds. Then read your project’s Webpack config and trace one file from entry to output.