What is a JavaScript bundler anyway?
A tool people struggle with for hours just to get a basic web app setup? A thing that you use when you bootstrap your React project? Something that your company uses, or that your colleagues/seniors have configured already, which is apparently supposed to optimize your final JS build?
Whether you are starting your web development journey or have already used a bunch of bundlers before, you might have had these questions at some point in time, I certainly did.
To answer the above question, a module bundler provides a method for arranging and merging multiple JavaScript files into a unified single file. Using a JavaScript bundler becomes necessary when your project outgrows a single file or when dealing with libraries with numerous dependencies. As a result, the end-user's browser or client doesn't have to fetch numerous files individually.
In this blog post, we'll go through what bundlers do and how they work in detail, but before that, why do we even need to combine dependencies into a single file? Let's have a look.
Table of contents
The Problem
To understand the core problem, let's consider a very simple traditional web app, with HTML, CSS and a script tag injecting an index.js
which acts as the entry point for JavaScript. To add some styling we're also injecting some external UI library like Pico CSS via a CDN and linking the same.
<html>
<head>
<script type="module" src="index.js"></script>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css"
/>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Very Simple Traditional App</title>
</head>
<body>
<button id="redirect-page-btn">Redirect</button>
<button id="say-hello">Say Hi</button>
</body>
</html>
Now we'll obviously not chunk all our JavaScript in a single index.js
file rather we'd divide our functionalities into individual files (as and when required). For this example, we'll create an alert and one redirect function in alert.js
and redirect.js
respectively.
// redirect.js
export function Redirect() {
window.location = "https://exampleURL.com/";
}
export function RandomFunction1() {
// This is an unused function
}
// alert.js
export function SayHi() {
alert("Hello Dev.to community");
}
export function RandomFunction2() {
// This is an unused function
}
In the two files above we have two unused functions RandomFunction1
and RandomFunction2
, which we'll get to in a moment but for now, let them hang out there.
Next, let's import the functions we created in index.js
and add some interactivity to the HTML elements.
// index.js
import { Redirect } from "./redirect.js";
import { SayHi } from "./alert.js";
document
.getElementById("redirect-page-btn")
.addEventListener("click", Redirect);
document.getElementById("say-hello").addEventListener("click", SayHi);
So far our application looks something like this, check the network tab closely, you'd find all the JS chunks referenced/loaded separately.
Unfortunately, it loads the whole script along with all the functions that aren't even in use, i.e. RandomFunction1
and RandomFunction2
.
When you consider libraries/packages that you import in your regular application like Loadash, Math, etc where you require just one or two functions for a specific use-case, you can understand the amount of unnecessary network requests it'd make to fetch every single file along with fetching everything your page doesn't need. Here’s where a JavaScript bundler comes into the field.
How bundlers bundle
Circling back to our discussion on bundlers and their pivotal role in modern web development, let's explore the inner workings of these tools in handling dependencies. Essentially, a bundler's operation can be broken down into two primary stages: generating a dependency graph and subsequently bundling the required elements.
Mapping a Dependency Graph
The initial step in module bundling involves creating a map that outlines the relationships among all the served files, known as Dependency Resolution. To do this, the bundler requires an entry file which should ideally be your main file. It then parses through this entry file to understand its dependencies.
Subsequently, the bundler navigates through these dependencies, tracing further dependencies if any, and allocates distinct IDs to every encountered file. Finally, it extracts all dependencies and generates a dependency graph that depicts the relationship between all files.
Why make a simple process so complicated? Good question
- When a browser requests functions from the bundler it can easily return the requested function because of the already constructed dependency order.
- Since JS bundlers have a good source map of all the files and their dependencies, it prevents name conflicts.
- It detects unused files allowing you to get rid of them if you choose.
Bundling
Once the bundler gets all the necessary inputs and sorts out its dependencies in the Dependency Resolution phase, it starts preparing the assets that the browser can handle without any hitch. This preparation stage is what we call Packing. In this step, the bundler cleverly uses the dependency graph to blend our numerous code files, plug in the needed functions and module.exports
object, and whip up a neat and tidy bundle that the browser will happily load up.
Bundlers are not transpilers
Firstly, let's clarify their roles. A JavaScript bundler is a tool that helps manage and organize JavaScript code and its dependencies, combining multiple files into a single, efficient bundle that can be easily loaded by the browser. The goal is to minimize HTTP requests and improve page load performance. On the other hand, a transpiler, short for "transformation compiler," is a tool that converts source code written in one programming language (such as ES6+ JavaScript) into another, usually older or more widely supported version (like ES5 JavaScript), making it compatible with a broader range of browsers.
These tools take a bunch of code in one language, and 'compile' it to another language. They're called commonly 'transpilers' rather than 'compilers' because unlike traditional compilers, these tools don't compile to a lower-level representation; they're just different languages at a similar level of abstraction.
These are typically used to run code written against newer JS versions in older JS runtimes (eg. Babel) or to provide custom languages with more conveniences or constraints that can then be executed in any regular JS environment (TypeScript, CoffeeScript).
Conclusion
Lately, developers have come up with some impressive alternatives to JavaScript bundling. I suggest keeping an eye on these options, as webpack is no longer the only go-to choice. Although webpack remains prominent, especially in tools like Create React App (CRA), which is a common approach for starting new React projects, there's flexibility to explore other options.
I recommend giving Parcel, esbuild, and ViteJS a look. These are modern bundlers designed, among other things, to alleviate certain challenges associated with webpack.
That’s all that you need to know about JavaScript bundlers to be able to work with them and understand what’s happening when executing this magic npm run build
script
Top comments (1)
Nice job! I am always confused about bundlers now I am clear.