Bundlers

& friends

The era of new javascript toolings

Table of Contents

1

Why?

2

Javascript modules

3

Module bundlers & friends

4

Use case

Why?

Retrospective

Why?

38%

JS

31%

API

31%

Others

Today

screenshot

Why?

Goal

<script type="application/javascript" src="vendors.js"></script>
<script type="application/javascript" src="app.js"></script>

Why?

#!/usr/bin/env bash

bundle_file="bundle.js"

deps=(
	"module_a/a.js"
	"module_b/b.js"
	"module_c/c.js"
)

for i in "${deps[@]}"; do
	[[ -f "${i}" ]] && cat "${i}" >> "${bundle_file}"
done

How

This was good in 2005

Javascript

Evolutions

Modules

Javascript Modules

Maintainability

Reusability

Ease of testing

Javascript Modules

I.I.F.E./Global module

N/A

const module = (() => {
  /* ... */
})()

CommonJS (CJS)

~2009

// module.js
const module = {
  /* ... */
}
module.exports = module
// app.js
// import full module
const module = require('./module')
// import single functionality
const { method } = require('./module')

AMD

~2009

// module.js
define('module', ['dep'], 
  function(dep) {
    /* ... */
})
// app.js
define(['module'], function(module) {
  /* ... */
})
here

Javascript Modules

I.I.F.E./Global module

N/A

const module = (() => {
  /* ... */
})()

Cannot import parts of modules

Modules needs to be "fully loaded" in right order

Might generate naming collisions

Works on browsers and server side

Javascript Modules

CommonJS (CJS)

~2009

// module.js
const module = {
  /* ... */
}
module.exports = module
// app.js
// import full module
const module = require('./module')
// import single functionality
const { method } = require('./module')

Most popular with hug number of existing module (npm)

Can import part of module

Synchronous import only

Works natively on server side only (node.js)

Better syntax

Javascript Modules

AMD

~2009

// module.js
define('module', ['dep'], 
  function(dep) {
    /* ... */
})
// app.js
define(['module'], function(module) {
  /* ... */
})
here

Cannot import parts of modules

Very verbose and convoluted syntax

Asynchronous modules loading

Works on Browsers and Server side

Javascript Modules

UMD

~2011

(function (root, factory) {
  if (typeof define === "function" && define.amd) {
    define(["dep"], factory);
  } else if (typeof exports === "object") {
    module.exports = factory(require("dep"));
  } else {
    root.module = factory(root.dep);
  }
}(this, function (dep) {
  const module = {/* ... */}
  return module
}));

ESM

~2015

// module.js
const method1 = () => /* .. */
const method2 = () => /* .. */
export { method1, method2 }
// app.js
// import full module
import module from './module'
// import single functionality
import { method1 } from './module'

Javascript Modules

UMD

~2011

(function (root, factory) {
  if (typeof define === "function" && define.amd) {
    define(["dep"], factory);
  } else if (typeof exports === "object") {
    module.exports = factory(require("dep"));
  } else {
    root.module = factory(root.dep);
  }
}(this, function (dep) {
  const module = {/* ... */}
  return module
}));

Complex, the wrapper code is almost impossible to write manually

Allows you to define modules taht ca be used by almost any module loader

Javascript Modules

ESM

~2015

// module.js
const method1 = () => /* .. */
const method2 = () => /* .. */
export { method1, method2 }
// app.js
// import full module
import module from './module'
// import single functionality
import { method1 } from './module'

Better syntax (close to CJS) BUT import & export are static

Standard format (part of ES2015)

Asynchrone (close to AMD)

Can import part of module

Works seamlessly in browsers & servers

Javascript Modules

Current most popular

  • CommonJS (CJS)
    • Mainly due to Node.js history
  • ESM
    • Standard
    • Best of every worlds

Javascript Modules

Why CJS still default in NodeJS?

ESM changes a bunch of stuff in JavaScript. ESM scripts use Strict Mode by default (use strict), their this doesn't refer to the global object, scoping works differently, etc.

Switching the default from CJS to ESM would be a big break in backwards compatibility. (Deno, the hot new alternative to Node, makes ESM the default, but as a result, its ecosystem is starting from scratch).

Module

& strategies

bundlers

Reminder

Parsing

Loading

Executing

Less code!

Less code = Less to transfer from network + less to parse + less to load + less to execute = 😻

Minification

Minification (mangling)

function hello(message) {
  console.log(message);
}

hello("Martin");
function n(a){console.log(a)}n("Martin");

Minification

JSMin

2001

Packer

2004

ShrinkSafe

2007

here

Regular expression

YUI Compressor

2007

Closure compiler

2009

Parser (AST)

Minification

Uglify

2011

Terser

2019

SWC

2020

here

esbuild

2020

Concatenation

Bundling

Bundling forerunners

Closure compiler

2009

Dojo Builder

2010

RequireJS

2010

here

Browserify

2011

JS based bundlers*

Webpack

2009

Rollup

2010

Parcel

2010

here

* Rollup v4 now used SWC parser

PROD bundle

85.3% used it

43% used it

23.5% used it

Vite

78.% used it

Next gen bundlers*

esbuild

2020

SWC

2020

Turbopack

2022

here

* Spoiler alert, isn't using JS

DEV bundle

49.6%

Vite

78.% used it

Biome

(formelly Rome)

2021

rspack

2023

used it

24.9%

used it

9.6%

used it

19.1%

used it

3.3%

used it

Underdog rookie bundler

Rolldown

2025

here

Will replace esbuild & rollup

Vite

78.% used it

1.5% used it

Scope hoisting

// lib.js
export function add(a, b) {
  return a + b;
}
// main.js
import { add } from "./utils.js";
console.log(add(2, 3));
var __utils__ = (function() {
  function add(a, b) {
    return a + b;
  }
  return { add };
})();

var __main__ = (function(utils) {
  console.log(utils.add(2, 3));
})(__utils__);
function add(a, b) {
  return a + b;
}

console.log(add(2, 3));

Bundling without scope hoisting

Bundling with scope hoisting

😊

😭

Tree shaking

Tree shaking

// two-and-three.js
import { one } from "./one.js";
export function two() {
  return one() + 1;
}
export function three() {
  return one() + 2;
}
// main.js
import { two } from "./two-and-three.js";
console.log(two());
function one() {
  return 1;
}
function two() {
  return one() + 1;
}
function three() {
  return one() + 2;
}
console.log(two());
function one() {
  return 1;
}
function two() {
  return one() + 1;
}
console.log(two());

Without tree-shaking

With tree-shaking

😊

😭

// one.js
export function one() {
  return 1;
}

Code splitting

Without code splitting

with code splitting

Code splitting (shared components)

Import hoisting

// one.js
export function one() {
  return 1;
}
// two.js
import { one } from './one.js'
export function two() { 
  return one() + 2;
}
// index.js
import { two } from './two.js'
console.log(two());
// index.js
import { two } from './two-bunlde.js';
console.log(two());
// index.js
import { two } from './two-bunlde.js';
import './one-bundle.js';
console.log(two());

Without import  hoisting

With import  hoisting

Content hashing

foo.js

foo-jdZxkbLl.js

foo.js

foo-BUojTdor.js

Asset inlining

// index.js
import logo from "./logo.svg";
console.log(`logo: ${logo}`);
var logo = "./logo-IJARBYVW.svg";
console.log(`logo: ${logo}`);
var logo = `data:image/svg+xml,
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" 
  stroke-miterlimit="10" 
  viewBox="142.92 136.23 738.38 729.8">
  ...    
</svg>%0A`;

// index.js
console.log(`logo: ${logo}`);

Without asset inlining

With asset  inlining

Asset loading

preload vs modulepreload vs prefetch

Attribute Main Purpose Loaded When? Typical Use Cases
preload Load critical resources immediately As soon as possible CSS, images, fonts, videos
modulepreload Load a JavaScript module and dependencies As soon as possible ES Modules scripts
prefetch Load resources for future use In the background Next pages, images

Use

& solutions

case

Agenda

  • Debugger network tool
  • Compression
  • vite-bundle-analyzer
  • Manual chunk
  • Dynamic import
  • Dynamic import prefetch

Bundlers & friends

By kakawait

Bundlers & friends

  • 91