🔌 Loaders and Plugins
Out of the box, webpack reads only JavaScript and JSON. Yet real apps import CSS, Sass, images, fonts, and modern syntax. Loaders teach webpack to read those file types one at a time, and plugins hook into the whole build to do broader work. Together they turn a bare bundler into a complete asset pipeline.
Week 3 · Day 3 (Wednesday: Webpack Basics) · Lecture 3
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Explain the difference between a loader and a plugin, and when to reach for each
- Configure the CSS, Sass, Babel, and asset pipelines with
module.rules - Predict loader execution order (right-to-left) and read a rule's
testregex - Use core plugins:
HtmlWebpackPlugin,MiniCssExtractPlugin,DefinePlugin - Handle images and fonts with webpack 5's built-in asset modules
- Assemble a realistic, production-ready config combining loaders and plugins
Estimated Time: 65 minutes
Project: Build a config that bundles JS, styles, and images, and auto-generates the HTML.
In This Lesson
The Assembly Line
Picture a car factory. The base assembly line (vanilla webpack) can bolt together JavaScript parts, but it can't paint the body, wire the stereo, or fit the tires. For those you bring in specialized stations — and that's the split between loaders and plugins. Loaders are the workers who transform one part as it moves down the line; plugins are the managers who oversee the whole floor and can change how the finished car is shipped.
transform each file] B --> C[webpack core
builds the graph] C --> D[Plugins
act on the whole build] D --> E[Bundle output]
Keep that one-line distinction in your pocket for the whole lesson: loaders operate on individual files as they enter the graph; plugins operate on the build as a whole. Everything else is detail hanging off those two roles.
What Loaders Do
Webpack natively understands only JavaScript and JSON. A loader is a transformation that turns some other kind of file into something webpack can fold into the dependency graph. Import a CSS file and, without a loader, webpack simply doesn't know what to do — it throws "You may need an appropriate loader to handle this file type."
Modern apps lean on many file types, each needing its own loader (or built-in handling):
- CSS / Sass / Less for styling
- Images (PNG, JPG, SVG, GIF) and fonts (WOFF, TTF)
- TypeScript, JSX, or bleeding-edge JavaScript that needs transpiling
- Data formats like CSV or Markdown
use: ['style-loader', 'css-loader'], css-loader processes the file first and hands its result to style-loader.Loader Order & Anatomy
The rule that trips up every beginner: loaders in a use array run from right to left (equivalently, bottom to top). Think of it as function composition — style-loader(css-loader(file)): the innermost, rightmost loader touches the raw file first.
// ❌ Wrong order — style-loader would try to run before CSS is parsed
use: ['css-loader', 'style-loader']
// ✅ Correct — css-loader parses first, style-loader injects last
use: ['style-loader', 'css-loader']
// ↑ runs 2nd ↑ runs 1st
Every rule is an object. The pieces you'll use constantly:
⚠️ test is a regex, not a string
test: '.js' // ❌ matches any filename CONTAINING ".js" anywhere
test: /\.js$/ // ✅ matches filenames ENDING in ".js"
The $ anchors the match to the end. Forgetting it is a classic source of "why is this loader running on the wrong files?" bugs.
The Common Loaders
1. Styling: CSS and Sass
Two loaders team up for CSS. css-loader reads the file and resolves its @import/url() references into the module graph; style-loader then injects those styles into the page by creating <style> tags at runtime.
npm install --save-dev css-loader style-loader
module.exports = {
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'], // right-to-left
},
],
},
};
// Now you can import CSS straight into your JavaScript:
import './styles.css';
const header = document.createElement('h1');
header.className = 'header';
header.textContent = 'Styled with webpack!';
document.body.appendChild(header);
For Sass, add sass-loader at the end of the array so it compiles .scss down to CSS before the other two run:
npm install --save-dev sass sass-loader css-loader style-loader
{
test: /\.s[ac]ss$/i, // matches .scss and .sass
use: [
'style-loader', // 3rd: inject into the DOM
'css-loader', // 2nd: resolve imports/urls
'sass-loader', // 1st: compile Sass → CSS
],
}
2. Modern JavaScript: babel-loader
Babel transpiles modern syntax down to a form older browsers understand, so you can write today's JavaScript without abandoning yesterday's users. babel-loader runs Babel on each matched file as it enters the graph.
npm install --save-dev babel-loader @babel/core @babel/preset-env
{
test: /\.js$/,
exclude: /node_modules/, // never transpile dependencies — slow and unnecessary
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'], // "compile down to what browsers need"
},
},
}
💡 Tip: Prefer ababel.config.jsonfile for Babel options once they grow — it keepswebpack.config.jsfocused on bundling and lets other tools (like Jest) share the same Babel settings. You'll go deeper on Babel in the next lesson on transpilation.
Images & Fonts (Asset Modules)
In webpack 4 you needed file-loader, url-loader, or raw-loader for images and fonts. Webpack 5 built these in as asset modules — no extra package required. You pick a type instead of a loader:
type | What it does | Replaces |
|---|---|---|
asset/resource | Emits a separate file, gives you its URL | file-loader |
asset/inline | Inlines the file as a base64 data URL | url-loader |
asset/source | Injects the raw file contents as a string | raw-loader |
asset | Auto-picks resource vs inline by file size | url-loader w/ limit |
module.exports = {
module: {
rules: [
{
test: /\.(png|svg|jpe?g|gif)$/i,
type: 'asset/resource', // copy to dist, return the URL
},
{
test: /\.(woff2?|eot|ttf|otf)$/i,
type: 'asset/resource',
},
{
test: /\.svg$/i,
type: 'asset', // inline small SVGs, emit big ones
parser: { dataUrlCondition: { maxSize: 8 * 1024 } }, // 8KB cutoff
},
],
},
};
// Importing an image returns its final URL:
import logoUrl from './logo.png';
const img = new Image();
img.src = logoUrl; // e.g. "/logo.8e0d62a8.png"
document.body.appendChild(img);
💡 Inline vs emit — the tradeoff
Inlining a tiny icon as a data URL saves an HTTP request. Inlining a large photo bloats your JavaScript bundle and hurts caching. The asset type with a maxSize threshold gives you the best of both automatically — small files inline, large files get their own cacheable URL.
What Plugins Do
Where a loader transforms one file, a plugin taps into webpack's compilation lifecycle and can act on the entire build — generating new files, injecting variables, extracting or minifying output, analyzing bundle size. Plugins are the managers with a view of the whole factory floor.
Two mechanical differences from loaders to burn in:
- Plugins live in a top-level
plugins: []array, not insidemodule.rules - Plugins are instantiated with
new, because most accept options
// ❌ Wrong — passing the class itself
plugins: [HtmlWebpackPlugin]
// ✅ Right — a new instance, optionally configured
plugins: [new HtmlWebpackPlugin({ title: 'My App' })]
Essential Plugins
1. HtmlWebpackPlugin — generate the HTML
With hashed filenames like main.8e0d62a8.js, hand-writing a <script> tag is hopeless — the name changes every build. This plugin generates an HTML file and injects the correct tags automatically.
npm install --save-dev html-webpack-plugin
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
title: 'My App',
template: './src/index.html', // your own template, optional
}),
],
};
// The output index.html automatically includes:
// <script src="main.8e0d62a8.js" defer></script>
2. MiniCssExtractPlugin — real CSS files
style-loader injects CSS via JavaScript, which is fine in development but means styles can't load in parallel or be cached separately. For production, this plugin extracts CSS into its own .css files. Note it comes with a loader that replaces style-loader in the chain.
npm install --save-dev mini-css-extract-plugin
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
module: {
rules: [
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader'], // swap out style-loader
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css', // cache-busted CSS
}),
],
};
3. DefinePlugin — inject build-time constants
A webpack built-in (no install) that replaces identifiers with values at build time — the standard way to feed an API URL or environment flag into your code.
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.DefinePlugin({
'process.env.API_URL': JSON.stringify('https://api.example.com'),
}),
],
};
// In your app, process.env.API_URL becomes the literal string at build time.
⚠️ Wrap DefinePlugin values in JSON.stringify
DefinePlugin does a raw text substitution. Without JSON.stringify, 'https://api.example.com' would be pasted as bare code (a syntax error), not as a quoted string. JSON.stringify adds the quotes for you.
A Real-World Config
Here's everything from this lesson working together — an environment-aware config that transpiles JS, handles CSS/Sass and assets, extracts CSS in production, and generates the HTML.
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = (env, argv) => {
const isProduction = argv.mode === 'production';
return {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: isProduction ? '[name].[contenthash].js' : '[name].js',
clean: true,
},
module: {
rules: [
// Modern JavaScript
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: { presets: ['@babel/preset-env'] },
},
},
// Styles — extract in prod, inject in dev
{
test: /\.s?css$/i,
use: [
isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
'css-loader',
'sass-loader',
],
},
// Images & fonts via built-in asset modules
{
test: /\.(png|svg|jpe?g|gif|woff2?|ttf|eot)$/i,
type: 'asset/resource',
},
],
},
plugins: [
new HtmlWebpackPlugin({ template: './src/index.html' }),
// Only extract CSS in production; filter(Boolean) drops the false in dev
isProduction && new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
].filter(Boolean),
devServer: {
static: './dist',
hot: true,
port: 3000,
},
devtool: isProduction ? 'source-map' : 'eval-source-map',
};
};
Running it
$ npm run build
asset main.8e0d62a8.js 14.2 KiB [emitted] [immutable]
asset main.4f1c9b2d.css 2.1 KiB [emitted] [immutable]
asset index.html 0.4 KiB [emitted]
webpack compiled successfully
Practice & Quiz
🏋️ Exercise 1: Fix the Sass pipeline
Goal: This rule is meant to compile .scss, resolve imports, and inject the result — but the styles never appear and the build errors. Find both bugs.
{
test: '.scss',
use: ['sass-loader', 'css-loader', 'style-loader'],
}
💡 Hint
One bug is in test (it should be a regex). The other is loader order — remember loaders run right to left, and Sass must be compiled to CSS before css-loader and style-loader can do their jobs.
✅ Solution
{
test: /\.scss$/i,
use: ['style-loader', 'css-loader', 'sass-loader'],
}
The array is reversed: sass-loader (rightmost) compiles first, then css-loader, then style-loader injects. And test must be the anchored regex /\.scss$/i.
🏋️ Exercise 2: Loader or plugin?
Goal: For each task, decide whether it needs a loader or a plugin, and name a candidate.
- Compile TypeScript files to JavaScript
- Generate an
index.htmlthat references the hashed bundle - Inject a different API URL for production vs development
- Turn imported PNGs into emitted files with hashed names
✅ Solution
1. Loader — ts-loader (or babel-loader) transforms each .ts file. 2. Plugin — HtmlWebpackPlugin acts on the whole build to emit HTML. 3. Plugin — DefinePlugin injects build-wide constants. 4. Neither a classic loader nor plugin — webpack 5's built-in type: 'asset/resource' handles it (it replaced file-loader). The tell: file-by-file transform → loader/asset type; whole-build action → plugin.
🎯 Quick Quiz
Question 1: In use: ['style-loader', 'css-loader'], which loader processes the file first?
Question 2: What's the core difference between a loader and a plugin?
Question 3: In webpack 5, how do you handle imported images without extra packages?
Best Practices & Pitfalls
✅ Do
- Remember loaders run right to left and order the array accordingly
- Anchor
testregexes with$(e.g./\.css$/i) exclude: /node_modules/frombabel-loader— never transpile dependencies- Use webpack 5 asset modules instead of the old file/url/raw loaders
- Extract CSS with
MiniCssExtractPluginin production, inject withstyle-loaderin dev - Instantiate plugins with
new
❌ Don't
- Put loaders in the
pluginsarray or vice versa - Forget
JSON.stringifyaround DefinePlugin string values - Use a plain string for
test— it matches far more than you expect - Run
MiniCssExtractPlugin.loaderandstyle-loadertogether in the same rule - Reach for a plugin when a simple loader (or asset type) does the job
✅ The mental model, one more time
Transforming a file type as it enters the graph? That's a loader (or an asset type). Doing something to the build as a whole — emitting files, injecting constants, minifying, analyzing? That's a plugin. When you're unsure which you need, ask "one file, or the whole build?"
Summary
🎉 Key Takeaways
- Loaders transform individual files so webpack can bundle non-JS types; they live in
module.rules - Loaders in a
usearray run right to left, andtestis an anchored regex - Core loaders:
css-loader+style-loader,sass-loader,babel-loader - Webpack 5 asset modules (
type: 'asset/resource'etc.) replace file/url/raw loaders - Plugins act on the whole build, live in
plugins: [], and are created withnew - Essential plugins:
HtmlWebpackPlugin,MiniCssExtractPlugin,DefinePlugin
📚 Additional Resources
- webpack — Loaders
- webpack — Plugins
- webpack — Asset modules (images & fonts)
- webpack — HtmlWebpackPlugin
- webpack — MiniCssExtractPlugin
🚀 What's Next?
You wired babel-loader into the build without really unpacking what Babel does. The next lesson, JavaScript Transpilation Concepts, zooms in on that: how modern syntax is rewritten for older browsers, what presets and polyfills are, and why transpilation sits at the heart of the modern toolchain.
🎉 Your pipeline is complete!
Loaders read every file type, plugins shape the whole build. You can now configure webpack for a real project — next we look under the hood of the transpiler that made it all modern.