Unpacking Webpack For Frontend Developers (Part 1)

mideseniordevmideseniordev
10 min read

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:

  1. Initialize the project:
mkdir webpack-demo
cd webpack-demo
pnpm init
  1. Install Webpack and dependencies:
pnpm add -D webpack webpack-cli
  1. 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 and url() like import/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:

  1. asset/resource: This loader copies image files into the output directory and returns their URLs for use in the code.

  2. 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!

32
Subscribe to my newsletter

Read articles from mideseniordev directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

mideseniordev
mideseniordev