Unpacking Webpack For Frontend Developers (Part 1)


If you’ve worked with React.js or Vue.js, you’ve probably encountered this familiar scenario: you type npm run build
into your terminal, and like magic, scripts run, and a build folder filled with neatly packed bundles appears. Everything works, and your app is ready for production—but have you ever wondered what really goes on behind the scenes?
That’s where Webpack comes in. It’s one of those tools that a lot of developers use every day without really knowing what’s going on under the hood. With its powerful module bundling capabilities, Webpack helps developers manage the complexities of modern web applications by organizing and optimizing assets for production.
In this article, we’re going to dive into Webpack, demystifying the processes of bundling and other core concepts that make it essential for modern web development. By the end, you’ll not only understand what happens when you hit build, but you’ll also gain the tools to make smarter choices when configuring Webpack for your own projects. So, let’s crack open Webpack and explore why it’s become such a crucial tool in the frontend developer's toolkit.
Introduction to Webpack
At its core, Webpack is a static module bundler designed to handle modern JavaScript applications. It builds a dependency graph, which is essentially a map that shows how all the different modules in your project are connected. This process starts at one or more entry points, typically the main JavaScript file of your app.
Webpack reads this file and checks which other files it depends on. It continues this process recursively until it identifies every file in your project that’s somehow linked. Webpack handles everything from JavaScript to CSS, ensuring your app is bundled efficiently.
In this hands-on article, you’ll learn how Webpack works by building a project step-by-step. We'll go through setting up your project with pnpm, installing Webpack, and configuring it to demonstrate core concepts like bundling, code splitting, and more.
Initial Setup
Let’s initialize a basic project using pnpm and install the required Webpack packages:
- Initialize the project:
mkdir webpack-demo
cd webpack-demo
pnpm init
- Install Webpack and dependencies:
pnpm add -D webpack webpack-cli
- Project Structure:
webpack-demo/
├── node_modules
├── package.json
├── pnpm-lock.yaml
├── public
| └── index.html
└── src
├── index.js
└── styles.css
You’ll be building alongside as we go deeper into Webpack's core concepts and advanced features!
Webpack Core Concepts
Before diving into the specifics, let's explore some of Webpack's core concepts. Understanding these fundamentals is key to leveraging Webpack effectively.
Configuration File
While Webpack can bundle projects without a configuration file (since version 4.0.0), most projects benefit from having one for more complex setups.
Create a configuration file webpack.config.js
in the root directory of your project:
webpack.config.js
module.exports = {}
Entry
Webpack uses an entry point (or multiple entry points) to begin building its dependency graph. The default entry point is ./src/index.js
, but you can specify a different one in your configuration.
For example:
webpack.config.js
module.exports = {
entry: './src/index.js',
};
You can also specify multiple entries:
webpack.config.js
module.exports = {
entry: {
app: ".src/index.js",
adminApp: "/src/admin/index.js"
},
};
We'll use the first example for our project.
Output
The output
property tells Webpack where to emit the bundled files. By default, Webpack creates a ./dist/main.js
file in a ./dist
folder. Let's configure this in webpack.config.js
:
webpack.config.js
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js"
}
};
Loaders
Webpack only understands JavaScript and JSON out of the box. Loaders allow Webpack to process other file types, like CSS or TypeScript, and convert them into valid modules.
For instance, to load .css
files, we’ll need the css-loader
.
Install it:
pnpm add -D css-loader
Then, add the loader to your webpack.config.js
:
webpack.config.js
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js"
},
module: {
rules: [
{test: /\.css$/, use: "css-loader"}
]
}
};
Plugins
Plugins extend Webpack's functionality beyond file transformation, providing features like optimization or file management. For example, the html-webpack-plugin
automatically injects bundled JavaScript into an HTML file.
Install it:
pnpm add -D html-webpack-plugin
Add it to your configuration:
webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js"
},
module: {
rules: [
{test: /\.css$/, use: "css-loader"}
]
},
plugins: [new HtmlWebpackPlugin({ template: './public/index.html' })],
};
Exploring Webpack Features
Update the files in your project before moving on to advanced features.
public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Webpack Demo</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
src/styles.css
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
color: #333;
}
h1 {
text-align: center;
margin-top: 50px;
}
src/index.js
import './styles.css';
const app = document.getElementById('app');
app.innerHTML = `<h1>Hello, Webpack!</h1>`;
In package.json
, add a build command for Webpack:
package.json
"scripts": {
"build": "webpack",
"start": "webpack serve --open"
}
Install webpack-dev-server
to serve your app:
pnpm install -D webpack-dev-server
This build command tells webpack to bundle your application and create a dist
folder in your root directory. While the start command starts up a mini server to run your application.
Asset Management
Webpack can handle files like CSS, images, and fonts using loaders and plugins.
Loading CSS
There are two approaches to loading CSS in Webpack:
Injecting CSS via JavaScript: Import CSS directly into your JavaScript. This approach works by adding <style>
tags directly into our HTML at runtime.
Install style-loader
pnpm add -D style-loader
Add 'style-loader'
to webpack.config.js
:
webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js"
},
module: {
rules: [
{
test: /\.css$/i,
use:[ "style-loader", "css-loader" ]
}
]
},
plugins: [new HtmlWebpackPlugin({ template: './public/index.html' })],
};
The order in which the loaders are arranged is very important 'style-loader'
comes first and followed by 'css-loader'
. If this convention is not followed, webpack is likely to throw errors.
css-loader
: Interprets@import
andurl()
likeimport/require()
and resolves them.style-loader
: Injects CSS into the DOM by adding a<style>
tag.
Generating a Separate CSS File: Use MiniCssExtractPlugin
to generate a separate CSS file in production builds. This method improves performance since the browser can download and cache the CSS separately, without waiting for the JavaScript to load.
Install it:
pnpm add -D mini-css-extract-plugin
Modify webpack.config.js
to use MiniCssExtractPlugin
:
webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js"
},
module: {
rules: [
{
test: /\.css$/i,
use:[ MiniCssExtractPlugin.loader, "css-loader" ]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
}),
new MiniCssExtractPlugin({
filename: 'main.css'
}),
],
mode: 'production', // Switch to production mode for optimized builds
};
For simplicity, we’ll use CSS injection in this project.
Loading Images
Webpack allows for more efficient handling of images by importing them directly into JavaScript or CSS files rather than manually specifying paths in HTML or CSS. This method ensures that images are optimized, processed, and included in the final build, improving caching and performance.
Asset Modules for Image Management:
asset/resource
: This loader copies image files into the output directory and returns their URLs for use in the code.asset/inline
: This loader inlines small images as base64 URLs, reducing the number of HTTP requests.
For this guide, we will use the asset/resource
module to handle images.
Add asset/resource
to webpack.config.js
webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js"
},
module: {
rules: [
{
test: /\.css$/i,
use:[ "style-loader", "css-loader" ]
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
]
},
plugins: [ new HtmlWebpackPlugin({ template: './public/index.html' }) ],
};
Add an image to your project. The folder structure for the project might look like this:
webpack-article-demo
├── node_modules
├── package.json
├── pnpm-lock.yaml
├── public
| ├── assets
| | ├── fonts
| | └── images
| | ├── background.svg
| | └── my-image.png
| └── index.html
├── src
| ├── index.js
| └── styles.css
└── webpack.config.js
I added two image files /public/assets/images/background.svg
and /public/assets/images/my-image.png
.
src/index.js
import './styles.css';
import MyImage from '../public/assets/images/my-image.png'
const app = document.getElementById("app");
app.innerHTML = `<h1>Hello, Webpack!</h1>`;
const container = document.createElement("div");
container.className = "container";
app.appendChild(container);
const img = document.createElement("img");
img.src = MyImage;
container.appendChild(img);
src/styles.css
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
color: #333;
background: url("../public/assets/images/background.svg");
background-size: contain;
background-position: center;
}
h1 {
text-align: center;
margin-top: 50px;
}
.container {
width: 100%;
margin: auto;
overflow: hidden;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.container img {
width: 500px;
height: 500px;
border-radius: 50%;
}
Loading Fonts
Handling fonts with Webpack follows a similar process. You configure Webpack to handle font files (e.g., .ttf
, .woff
, .otf
) using the asset/resource
loader.
Modify webpack.config.js
file to load font files:
webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js"
},
module: {
rules: [
{
test: /\.css$/i,
use:[ "style-loader", "css-loader" ]
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: "asset/resource",
},
]
},
plugins: [ new HtmlWebpackPlugin({ template: './public/index.html' }) ],
};
Add some font files to your project. The folder structure for the project might look like this:
webpack-article-demo
├── node_modules
├── package.json
├── pnpm-lock.yaml
├── public
| ├── assets
| | ├── fonts
| | | └── Anton-Regular.ttf
| | └── images
| | ├── background.svg
| | └── my-image.png
| └── index.html
├── src
| ├── index.js
| └── styles.css
└── webpack.config.js
I added the font file /public/assets/fonts/Anton-Regular.tff
to the fonts folder.
src/styles.css
@font-face {
font-family: "Anton";
src: url("../public/assets/fonts/Anton-Regular.ttf") format("ttf");
font-weight: 400;
font-style: normal;
}
body {
font-family: "Anton";
background-color: #f0f0f0;
color: #333;
background: url("../public/assets/images/background.svg");
background-size: contain;
background-position: center;
}
h1 {
text-align: center;
margin-top: 50px;
}
.container {
width: 100%;
margin: auto;
overflow: hidden;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.container img {
width: 500px;
height: 500px;
border-radius: 50%;
}
By using the @font-face
directive and loading fonts through Webpack, the build process takes care of optimizing the fonts and injecting them into the final output.
Output Management
Let’s see how the typical output from a webpack build looks like. So far here is how our project structure looks like.
webpack-article-demo
├── node_modules
├── package.json
├── pnpm-lock.yaml
├── public
| ├── assets
| | ├── fonts
| | | └── Anton-Regular.ttf
| | └── images
| | ├── background.svg
| | └── my-image.png
| └── index.html
├── src
| ├── index.js
| └── styles.css
└── webpack.config.js
We’ll go ahead to make some adjustments to our webpack.config.js
file
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
mode: "development",
entry: './src/index.js',
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].bundle.js",
clean: true
},
module: {
rules: [
{
test: /\.css$/i,
use:[ "style-loader", "css-loader" ]
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: "asset/resource",
},
]
},
plugins: [ new HtmlWebpackPlugin({ template: './public/index.html' }) ],
};
Notice how the filename field under the output change from bundle.js
to [name].bundle.js
. This tells webpack to generate a bundle file using the name of the entry point. We also set our mode to development
which tells webpack to use its built-in development mode optimizations accordingly. Now we can run our first build, let’s run pnpm run build
and see what it generates.
assets by status 1.58 MiB [cached] 3 assets
assets by path . 38.4 KiB
asset main.bundle.js 38.2 KiB [emitted] (name: main)
asset index.html 287 bytes [emitted]
runtime modules 2.57 KiB 8 modules
javascript modules 12.5 KiB
modules by path ./node_modules/.pnpm/ 8.73 KiB
modules by path ./node_modules/.pnpm/style-loader@4.0.0_webpack@5.95.0_webpack-cli@5.1.4_/node_m...(truncated) 5.84 KiB 6 modules
modules by path ./node_modules/.pnpm/css-loader@7.1.2_webpack@5.95.0_webpack-cli@5.1.4_/node_mod...(truncated) 2.89 KiB
./node_modules/.pnpm/css-loader@7.1.2_webpack@5.95.0_webpack-cli@5.1.4_/node_mod...(truncated) 64 bytes [built] [code generated]
+ 2 modules
modules by path ./src/ 3.76 KiB
./src/index.js 376 bytes [built] [code generated]
./src/styles.css 1.66 KiB [built] [code generated]
./node_modules/.pnpm/css-loader@7.1.2_webpack@5.95.0_webpack-cli@5.1.4_/node_modules/css-loader/dist/cjs.js!./src/styles.css 1.73 KiB [built] [code generated]
asset modules 126 bytes (javascript) 1.58 MiB (asset)
./public/assets/images/my-image.png 42 bytes (javascript) 1.43 MiB (asset) [built] [code generated]
./public/assets/fonts/Anton-Regular.ttf 42 bytes (javascript) 158 KiB (asset) [built] [code generated]
./public/assets/images/background.svg 42 bytes (javascript) 1.57 KiB (asset) [built] [code generated]
webpack 5.95.0 compiled successfully in 225 ms
✨ Done in 0.94s.
Conclusion
In this first part of our Webpack exploration, we laid the groundwork by discussing essential concepts such as configuration files, entry points, output settings, loaders, and plugins. We also learned how to manage assets like images and fonts efficiently using the asset/resource
module, optimizing performance and simplifying our workflow.
Armed with these core concepts, we are now ready to delve into advanced features like code splitting, lazy loading, hot module replacement, and optimization techniques in the next section. Mastering both the basics and these advanced functionalities will enhance our ability to create efficient and scalable applications. Join us as we continue to unlock Webpack's powerful capabilities!
Subscribe to my newsletter
Read articles from mideseniordev directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
