Let's Talk Bundlers

Let's Talk Bundlers

A bundler is a tool that takes multiple JavaScript, CSS, and other assets and combines them into a single (or a few) optimised files for efficient loading in a web browser.

What Does This Mean?

Let’s break this down with an example.

Imagine you're building a calculator app in JavaScript. Instead of writing all your code in one massive file, you decide to split it into multiple files to keep things organised.

Project Structure

calculator-app/
│── index.html
│── style.css
│── index.js
│── math.js
│── utils.js

Each file handles different parts of the app:

  • index.html → Serves as the entry point for the browser

  • style.css → Manages the visual styling and layout of the app

  • math.js → Handles calculations

  • utils.js → Provides helper functions

  • index.js → Controls the app’s main logic

Logic Breakdown

1. math.js (Handles Math Operations)

export const add = (a, b) => {
  return a + b;
}

export const subtract = (a, b) => {
  return a - b;
}

2. utils.js (Helper Functions)

export const printResult = (result) => {
  console.log(`Result: ${result}`);
}

3. index.js (Main Application Logic)

import { add, subtract } from "./math.js";
import { printResult } from "./utils.js";

const result = add(5, 3);
printResult(result); // Output: Result: 8

Now, this setup seems fine, but as your project grows, you'll start facing challenges**.**

Problems Without a Bundler

  1. Compatibility Issues

    Older browsers (like Internet Explorer) don’t support ES module imports (import { add } from "./math.js";). You would need to manually include polyfills or rewrite your code to work with older JavaScript module systems (like CommonJS).

  2. Redundant Code (Unused Functions and Comments)

    Right now, our subtract() function is never used. But when the browser loads math.js, it still loads that function, wasting resources.

    Other unnecessary parts of the code include:

    • Comments that don’t need to be in the final file

    • Extra white spaces that make the file larger

  1. Too Many HTTP Requests

    Each import statement in index.js makes a separate request to the server.

    Imagine you have 50+ JavaScript files, your browser has to load each one individually, making 50+ network requests. This slows down page load speed.

  2. Dependency Management Issues

    If you use third-party libraries (like Lodash), you typically install them via npm.

    Let’s modify our calculator app to use Lodash:

    Updated index.js

     import { add, substract} from "./math.js";
     import { printResult } from "./utils.js";
     import _ from "lodash"; // Import Lodash (third-party library)
    
     const result = add(5, 3);
     printResult(result); // Output: Result: 8
    
     const numbers = [2, 4, 6, 8];
     const sum = _.sum(numbers); // Uses Lodash to sum numbers
     printResult(sum); // Output: Result: 20
    

    However, this will fail in the browser :

    This occurs because:

    1. Browsers don’t understand Node.js module resolution:

      • When you use import _ from "lodash", the browser doesn’t know how to resolve the lodash package from node_modules.

      • Browsers expect module paths to be relative (e.g., ./math.js) or absolute (e.g., /path/to/file.js).

    2. Lodash is installed in node_modules:

      • The lodash package is installed via npm and resides in the node_modules folder, which browsers cannot access directly.

How Does a Bundler Solves These Challenges?

A bundler processes your files and resolves the issues we identified earlier—making your app faster, more efficient, and browser-compatible. Let’s break down the process step by step, referencing our calculator app example.

Step 1: Entry Point Discovery

The first step in bundling is entry point discovery. The bundler starts from the main file (in this case, index.js) and systematically follows all import statements to determine which files and dependencies are required to run the application.

Updated index.js

import { add, substract} from "./math.js";
import { printResult } from "./utils.js";
import _ from "lodash"; // Import Lodash (third-party library)

const result = add(5, 3);
printResult(result); // Output: Result: 8

const numbers = [2, 4, 6, 8];
const sum = _.sum(numbers); // Uses Lodash to sum numbers
printResult(sum); // Output: Result: 20

Dependencies Identified by the Bundler:

  1. math.js → contains add() and subtract() functions

  2. utils.js → contains printResult()

  3. lodash → an external package providing the _.sum() function

Step 2: Creating a Dependency Graph

After discovering all dependencies in Step 1, the bundler constructs a dependency graph—a structured representation of how different files are interconnected. This ensures that no required module is left out and helps optimize how the code is bundled.

Given our index.js file:

import { add, subtract } from "./math.js";
import { printResult } from "./utils.js";
import _ from "lodash"; // Import Lodash (third-party library)

The bundler generates the following dependency graph:

index.js  
 ├── math.js  
 │   ├── (add function)  
 │   ├── (subtract function)  
 ├── utils.js  
 │   ├── (printResult function)  
 └── lodash (npm package)  
     ├── (sum function)

This visualises how index.js relies on math.js, utils.js, and lodash, and how each of these files contribute specific functions.

Step 3: Code Transformation

In this step, the bundler takes the modern JavaScript code (or other assets like CSS) and transforms it into a format that’s compatible with all browsers, even older ones. This is especially important because not all browsers support modern JavaScript features like ES modules (import/export).

What Happens Here?

  1. Transpiling Modern JavaScript:

    • The bundler uses tools like Babel or SWC to convert modern JavaScript (ES6+) into older versions (ES5) that older browsers can understand.

    • Arrow functions (() => {}) are converted into regular functions (function() {}).

    • const and let are converted to var.

    • Template literals (`Result: ${result}` ) are converted to String concatenation ("Result: " + result)

  2. Transforming import to require:

    • Modern import statements are converted into require statements, which are compatible with older environments.

    • Similarly, export statements are converted into module.exports.

  3. Handling Non-JavaScript Files:

    • If your project includes CSS, images, or other assets, the bundler will process these too. For example, it might convert SCSS into plain CSS or optimise image sizes.
  4. Resolving Module Paths:

    • The bundler ensures that all import statements are correctly resolved. For example, if you import lodash from node_modules, the bundler will locate the correct file and include it in the bundle.

Let’s use the calculator-app example to show how the bundler might transform the code.

Before Transformation (Modern JavaScript with ES Modules):

// math.js
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

// utils.js
export function printResult(result) {
  console.log(`Result: ${result}`);
}

// index.js
import { add } from "./math.js";
import { printResult } from "./utils.js";
import _ from "lodash";

const result = add(5, 3);
printResult(result);

After Transformation (CommonJS and ES5):

// Transformed math.js
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

module.exports = { add, subtract };

// Transformed utils.js
function printResult(result) {
  console.log("Result: " + result); // Template literal transformed
}

module.exports = { printResult };

// Transformed index.js
var math = require("./math.js");
var utils = require("./utils.js");
var _ = require("lodash");

var result = math.add(5, 3); // const transformed to var
utils.printResult(result);

Step 4: Code Optimisation

Once the code is transformed, the bundler optimises it to make it smaller, faster, and more efficient. This step ensures that your app loads quickly and doesn’t waste resources.

What Happens Here?

  1. Tree Shaking:

    The bundler removes unused code. For example, since the the subtract function in math.js is never used, it won’t be included in the final bundle. This reduces the file size.

  2. Minification:

    The bundler removes unnecessary characters like comments, whitespace, and long variable names. For example:

     // Before minification
     function add(a, b) {
       return a + b;
     }
    

    After minification:

     function add(a,b){return a+b}
    
  3. Code Splitting:

    Instead of generating one large file,, the bundler can split the code into smaller chunks (files) that load only when needed. This improves performance by reducing the initial load time.

  4. Optimising Dependencies:

    In scenarios where third-party libraries like Lodash are used, the bundler ensures only the parts needed are included. For example, since only the sum function from Lodash was used, the bundler will only include that and not the entire library.

Step 5: Generating the Final Output

After transforming and optimising the code, the bundler generates the final output—a single (or a few) bundled files that your browser can load efficiently.

What Happens Here?

  1. Creating the Bundle:

    The bundler combines all the transformed and optimised code into one or more files. For example, it might create a single bundle.js file that includes your app’s logic, dependencies, and assets.

  2. Generating Source Maps:

    Source maps are files that help you debug your code in the browser. They map the bundled code back to the original source files, so you can see where errors occurred in your original code.

  3. Outputting Files:

    The bundler saves the final files to a specified directory (e.g., dist/ or build/). These files are ready to be deployed to a web server.

Example:

After bundling, your project might look like this:

calculator-app/
│── dist/
│   ├── bundle.js
│   ├── style.css
│   ├── index.html
  • bundle.js contains all your JavaScript code, including dependencies like Lodash.

  • style.css contains all your CSS, optimised and minified.

  • index.html is updated to include the bundled files.

BundlerUsage
WebpackThe most powerful and configurable bundler, widely used in large projects.
ViteLightning-fast bundler optimised for modern frameworks like React and Vue.
ParcelZero-config bundler, great for beginners and fast builds.
RollupBest for libraries and optimising ES modules.
esbuildExtremely fast bundler written in Go, used for instant builds.

In Conclusion,

A bundler is an essential tool for modern web development, transforming how we build and optimise applications. By combining multiple JavaScript, CSS, and asset files into a single (or a few) optimised bundles, it addresses critical challenges like browser compatibility, redundant code, excessive HTTP requests, and dependency management.