Skip to main content

An Introduction to the Rollup.js JavaScript Bundler

By Craig Buckler

JavaScript

Share:

Free JavaScript Book!

Write powerful, clean and maintainable JavaScript.

RRP $11.95

Rollup.js is a next-generation JavaScript module bundler from Rich Harris, the author of Svelte. It compiles multiple source files into a single bundle.

The benefits include:

  • development is easier to manage when using smaller, self-contained source files
  • the source can be linted, prettified, and syntax-checked during bundling
  • tree-shaking removes unused functions
  • transpiling to ES5 for backward compatibility is possible
  • multiple output files can be generated — for example, your library could be provided in ES5, ES6 modules, and Node.js-compatible CommonJS
  • production bundles can be minified and have logging removed

Other bundler options, such as webpack, Snowpack, and Parcel, attempt to magically handle everything: HTML templating, image optimization, CSS processing, JavaScript bundling, and more. This works well when you’re happy with the default settings, but custom configurations can be difficult and processing is slower.

Rollup.js primarily concentrates on JavaScript (although there are plugins for HTML templates and CSS). It has a daunting number of options, but it’s easy to get started and bundling is fast. This tutorial explains how to use typical configurations within your own projects.

Install Rollup.js

Rollup.js requires Node.js v8.0.0 or above and can be installed globally with:

npm install rollup --global

This permits the rollup command to be run in any project directory containing JavaScript files — such as a PHP, WordPress, Python, Ruby or other project.

However, if you’re on a larger team creating a Node.js project, it can be preferable to install Rollup.js locally to ensure all developers are using the same version. Presuming you have an existing Node.js package.json file within a project folder, run:

npm install rollup --save-dev

You won’t be able to run the rollup command directly, but npx rollup can be used. Alternatively, rollup commands can be added to the package.json "scripts" section. For example:

"scripts": {
  "watch": "rollup ./src/main.js --file ./build/bundle.js --format es --watch",
  "build": "rollup ./src/main.js --file ./build/bundle.js --format es",
  "help": "rollup --help"
},

These scripts can be executed with npm run <scriptname> — for example, npm run watch.

The examples below specifically use npx rollup, since it will work regardless of whether rollup is installed locally or globally.

Example Files

Example files and Rollup.js configurations can be downloaded from GitHub. It’s a Node.js project, so run npm install after cloning and examine the README.md file for instructions. Note that Rollup.js and all plugins are installed locally.

Alternatively, you can create the source files manually after initializing a new Node.js project with npm init. The following ES6 modules create a real-time digital clock used to demonstrate Rollup.js processing.

src/main.js is the main entry point script. It locates a DOM element and runs a function every second, which sets its content to the current time:

import * as dom from './lib/dom.js';
import { formatHMS } from './lib/time.js';

// get clock element
const clock = dom.get('.clock');

if (clock) {

  console.log('initializing clock');

  // update clock every second
  setInterval(() => {

    clock.textContent = formatHMS();

  }, 1000);

}

src/lib/dom.js is a small DOM utility library:

// DOM libary

// fetch first node from selector
export function get(selector, doc = document) {
  return doc.querySelector(selector);
}

// fetch all nodes from selector
export function getAll(selector, doc = document) {
  return doc.querySelectorAll(selector);
}

and src/lib/time.js provides time formatting functions:

// time formatting

// return 2-digit value
function timePad(n) {
  return String(n).padStart(2, '0');
}

// return time in HH:MM format
export function formatHM(d = new Date()) {
  return timePad(d.getHours()) + ':' + timePad(d.getMinutes());
}

// return time in HH:MM:SS format
export function formatHMS(d = new Date()) {
  return formatHM(d) + ':' + timePad(d.getSeconds());
}

The clock code can be added to a web page by creating an HTML element with a clock class and loading the script as an ES6 module:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Rollup.js testing</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<script type="module" src="./src/main.js"></script>
</head>
<body>

  <h1>Clock</h1>

  <time class="clock"></time>

</body>
</html>

Rollup.js provides options for optimizing the JavaScript source files.

Rollup.js Quick Start

The following command can be run from the root of the project folder to process src/main.js and its dependencies:

npx rollup ./src/main.js --file ./build/bundle.js --format iife

A single script at build/bundle.js is output. It contains all code, but notice that unused dependencies such as the getAll() function in src/lib/dom.js have been removed:

(function () {
  'use strict';

  // DOM libary

  // fetch first node from selector
  function get(selector, doc = document) {
    return doc.querySelector(selector);
  }

  // time library

  // return 2-digit value
  function timePad(n) {
    return String(n).padStart(2, '0');
  }

  // return time in HH:MM format
  function formatHM(d = new Date()) {
    return timePad(d.getHours()) + ':' + timePad(d.getMinutes());
  }

  // return time in HH:MM:SS format
  function formatHMS(d = new Date()) {
    return formatHM(d) + ':' + timePad(d.getSeconds());
  }

  // get clock element
  const clock = get('.clock');

  if (clock) {

    console.log('initializing clock');

    setInterval(() => {

      clock.textContent = formatHMS();

    }, 1000);

  }

}());

The HTML <script> can now be changed to reference the bundled file:

<script type="module" src="./build/bundle.js"></script>

Note: type="module" is no longer necessary, so the script should work in older browsers which support early ES6 implementations. You should also add a defer attribute to ensure the script runs after the DOM is ready (this occurs by default in ES6 modules).

Rollup.js offers numerous command-line flags. The following sections describe the most useful options.

Rollup.js Help

Rollup’s command-line options can be viewed with the --help or -h flag:

npx rollup --help

The Rollup.js version can be output with --version or -v:

npx rollup --version

Output File

The --file (or -o) flag defines the output bundle file, which is set to ./build/bundle.js above. If no file is specified, the resulting bundle is sent to stdout.

JavaScript Formatting

Rollup.js provides several --format (or -f) options to configure the resulting bundle:

option description
iife wrap code in an Immediately Invoked Function Expression (function () { ... }()); block so it cannot conflict with other libraries
es6 standard ES6
cjs CommonJS for Node.js
umd Universal Module Definition for use on both the client and server
amd Asynchronous Module Definition
system SystemJS modules

Unless you’re using a specific module system, iife will be the best option for client-side JavaScript. es6 will produce a slightly smaller bundle, but be wary of global variables and functions which could conflict with other libraries.

Output a Source Map

A source map provides a reference back to the source files so they can be examined in browser developer tools. This makes it easier to set breakpoints or locate problems when errors occur.

An external source map can be created by adding a --sourcemap flag to the rollup command:

npx rollup ./src/main.js --file ./build/bundle.js --format iife --sourcemap

This creates an additional ./build/bundle.js.map file. You can view it, although it’s mostly gibberish and not intended for human consumption! The map is referenced as a comment at the end of ./build/bundle.js:

//# sourceMappingURL=bundle.js.map

Alternatively, you can create an inline source map with --sourcemap inline. Rather than producing an additional file, a base64-encoded version of the source map is appended to ./build/bundle.js:

//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY...etc...

After generating the source map, you can load an example page which references the script. Open your developer tools and navigate to the Sources tab in Chrome-based browsers or the Debugger tab in Firefox. You’ll see the original src code and line numbers.

Watch Files and Automatically Bundle

The --watch (or -w) flag monitors your source files for changes and automatically builds the bundle. The terminal screen is cleared on every run, but you can disable this with --no-watch.clearScreen:

npx rollup ./src/main.js --file ./build/bundle.js --format iife --watch --no-watch.clearScreen

Create a Configuration File

Command-line flags can quickly become unwieldy. The examples above are already long and you’ve not begun to add plugins!

Rollup.js can use a JavaScript configuration file to define bundling options. The default name is rollup.config.js and it should be placed in the root of your project (typically, the directory where you run rollup from).

The file is an ES module which exports a default object that sets Rollup.js options. The following code replicates the commands used above:

// rollup.config.js

export default {

  input: './src/main.js',

  output: {
    file: './build/bundle.js',
    format: 'iife',
    sourcemap: true
  }

}

Note: sourcemap: true defines an external sourcemap. Use sourcemap: 'inline' for an inline sourcemap.

You can use this configuration file when running rollup by setting the --config (or -c) flag:

npx rollup --config

A file name can be passed if you named the configuration something other than than rollup.config.js. This can be practical when you have multiple configurations perhaps located in a config directory. For example:

npx rollup --config ./config/rollup.simple.js

Automatic Bundling

watch options can be set within the configuration file. For example:

// rollup.config.js

export default {

  input: './src/main.js',

  watch: {
    include: './src/**',
    clearScreen: false
  },

  output: {
    file: './build/bundle.js',
    format: 'iife',
    sourcemap: true
  }

}

However, it’s still necessary to add a --watch flag when calling rollup:

npx rollup --config --watch

Process Multiple Bundles

The configuration file above returns a single object to process one input file and its dependencies. You can also return an array of objects to define multiple input and output operations:

// rollup.config.js

export default [

  {

    input: './src/main.js',

    output: {
      file: './build/bundle.js',
      format: 'iife',
      sourcemap: true
    }

  },

  {

    input: './src/another.js',

    output: {
      file: './build/another.js',
      format: 'es'
    }

  },

]

It may be practical to define an array even when returning a single object. This will make it easier to append further processes later.

Using Environment Variables

The configuration file is JavaScript, so settings can be changed according to any environmental factor. For example, you may want script bundling to be slightly different when running on your development machine or a production server.

The following configuration detects the NODE_ENV environment variable and removes the source map when it’s set to production:

// Rollup.js development and production configurations
const dev = (process.env.NODE_ENV !== 'production');

console.log(`running in ${ dev ? 'development' : 'production' } mode`);

const sourcemap = dev ? 'inline' : false;

export default [

  {

    input: './src/main.js',

    watch: {
      clearScreen: false
    },

    output: {
      file: './build/bundle.js',
      format: 'iife',
      sourcemap
    }

  }

]

The value of NODE_ENV can be set from the command line on macOS or Linux:

NODE_ENV=production

This is the Windows cmd prompt:

set NODE_ENV=production

For Windows Powershell:

$env:NODE_ENV="production"

However, Rollup.js also allows you to temporarily set/override environment variables in the --environment flag. For example:

npx rollup --config --environment VAR1,VAR2:value2,VAR3:x

process.env can then be examined in your configuration file:

  • process.env.VAR1 is true
  • process.env.VAR2 is value2
  • process.env.VAR3 is x

The configuration script above defaults to development mode, but production mode (without a source map) can be triggered with:

npx rollup --config --environment NODE_ENV:production

Rollup.js Plugins

Rollup.js has an extensive range of plugins to supplement the bundling and output process. You’ll find various options to inject code, compile TypeScript, lint files, and even trigger HTML and CSS processing.

Using a plugin is similar to other Node.js projects. You must install the plugin module, then reference it in a plugin array in the Rollup.js configuration file. The following sections describe several of the most-used plugins.

Use npm Modules

Many JavaScript libraries are packaged as CommonJS modules which can be installed using npm. Rollup.js can include such scripts in bundles with the following plugins:

  1. node-resolve, which locates the module in the project’s node_modules directory, and
  2. plugin-commonjs, which converts CommonJS modules to ES6 when necessary.

Install them in your project:

npm install @rollup/plugin-node-resolve @rollup/plugin-commonjs --save-dev

Rather than using the time formatting functions in src/lib/time.js, you could add a more comprehensive date/time handling library such as day.js. Install it with npm:

npm install dayjs --save-dev

Then modify src/main.js accordingly:

import * as dom from './lib/dom.js';
import dayjs from 'dayjs';

// get clock element
const clock = dom.get('.clock');

if (clock) {

  console.log('initializing clock');

  setInterval(() => {

    clock.textContent = dayjs().format('HH:mm:ss');

  }, 1000);

}

rollup.config.js must be updated to include and use the plugins in a new plugins array:

// Rollup.js with npm modules
import { nodeResolve as resolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';

const
  dev = (process.env.NODE_ENV !== 'production'),
  sourcemap = dev ? 'inline' : false;

console.log(`running in ${ dev ? 'development' : 'production' } mode`);

export default [

  {

    input: './src/main.js',

    watch: {
      clearScreen: false
    },

    plugins: [
      resolve({
        browser: true
      }),
      commonjs()
    ],

    output: {
      file: './build/bundle.js',
      format: 'iife',
      sourcemap
    }

  }

];

Run Rollup.js as before:

npx rollup --config

You’ll now find day.js code has been included within build/bundle.js.

Once you’re happy it’s working, revert src/main.js back to the original local code library, since it’s used in the following sections. Your rollup.config.js doesn’t need to change.

Replace Tokens

It’s often useful to pass configuration variables at build time so they become hard-coded in the bundled script. For example, you could create a JSON file with design tokens that specify colors, fonts, spacing, selectors, or any other tweaks that can be applied to HTML, CSS, or JavaScript.

The Rollup.js replace plugin can replace any reference in your scripts. Install it with:

npm install @rollup/plugin-replace --save-dev

Modify rollup.config.js to import the plugin and define a tokens object which is passed to the replace() function in the plugins array. In this example, you can modify the clock selector (__CLOCKSELECTOR__), update time interval (__CLOCKINTERVAL__), and time formatting function (__CLOCKFORMAT__):

// Rollup.js configuration
import { nodeResolve as resolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import replace from '@rollup/plugin-replace';

const
  dev = (process.env.NODE_ENV !== 'production'),
  sourcemap = dev ? 'inline' : false,

  // web design token replacements
  tokens = {
    __CLOCKSELECTOR__: '.clock',
    __CLOCKINTERVAL__: 1000,
    __CLOCKFORMAT__: 'formatHMS'
  };

console.log(`running in ${ dev ? 'development' : 'production' } mode`);

export default [

  {

    input: './src/main.js',

    watch: {
      clearScreen: false
    },

    plugins: [
      replace(tokens),
      resolve({
        browser: true
      }),
      commonjs()
    ],

    output: {
      file: './build/bundle.js',
      format: 'iife',
      sourcemap
    }

  }

];

src/main.js must be modified to use these tokens. Replacement strings can be applied anywhere — even as function names or import references:

import * as dom from './lib/dom.js';
import { __CLOCKFORMAT__ } from './lib/time.js';

// get clock element
const clock = dom.get('__CLOCKSELECTOR__');

if (clock) {

  console.log('initializing clock');

  setInterval(() => {

    clock.textContent = __CLOCKFORMAT__();

  }, __CLOCKINTERVAL__);

}

Run npx rollup --config and you’ll discover that build/bundle.js is identical to before, but it can now be modified by changing the Rollup.js configuration file.

Transpile to ES5

Modern JavaScript works in modern browsers. Unfortunately, that doesn’t include older applications such as IE11. Many developers use solutions such as Babel to transpile ES6 to a backward-compatible ES5 alternative.

I have mixed feelings about creating ES5 bundles:

  1. In December 2020, IE11’s market share was less than 1%. Inclusivity is great, but is it more beneficial to concentrate on accessibility and performance rather than a decade-old browser?
  2. Legacy browsers can be supported if progressive enhancement is adopted. Those browsers may not run any JavaScript, but the site can still offer a level of HTML and CSS functionality.
  3. ES5 bundles can be considerably larger than ES6. Should modern browsers receive a less efficient script?

Moving toward the future, I suggest you bundle ES6 only and have older (slower) browsers rely on HTML and CSS alone. That won’t always be possible — such as when you have a complex application with a large proportion of IE11 users. In those situations, consider creating both ES6 and ES5 bundles and serve the appropriate script.

Rollup.js offers a plugin which uses Bublé to transpile to ES5. The project is in maintenance mode but still works well.

Note: here’s a quote from the project repository: “Bublé was created when ES2015 was still the future. Nowadays, all modern browsers support all of ES2015 and (in some cases) beyond. Unless you need to support IE11, you probably don’t need to use Bublé to convert your code to ES5.”

Install the plugin so you can output both ES6 and ES5 modules:

npm install @rollup/plugin-buble --save-dev

Before modifying the configuration, the String padStart() function used in src/lib/time.js is not implemented in older browsers. A simple polyfill can be used by adding the following code to a new src/lib/polyfill.js file:

// String.padStart polyfill
if (!String.prototype.padStart) {

  String.prototype.padStart = function padStart(len, str) {

    var t = String(this);
    len = len || 0;
    str = str || ' ';
    while (t.length < len) t = str + t;
    return t;

  };

}

This polyfill is not required in ES6, so you require a way to inject it into the ES5 code only. Fortunately, you have already installed the replace plugin so this can be adopted for the task.

Add a __POLYFILL__ token to the top of src/main.js:

__POLYFILL__
import * as dom from './lib/dom.js';
import { __CLOCKFORMAT__ } from './lib/time.js';

// rest of code...

Then set it in the Rollup.js configuration in the ES5 "plugins" array:

// Rollup.js configuration
import replace from '@rollup/plugin-replace';
import { nodeResolve as resolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import buble from '@rollup/plugin-buble';

// settings
const
  dev = (process.env.NODE_ENV !== 'production'),
  sourcemap = dev ? 'inline' : false,

  input = './src/main.js',

  watch = { clearScreen: false },

  tokens = {
    __CLOCKSELECTOR__: '.clock',
    __CLOCKINTERVAL__: 1000,
    __CLOCKFORMAT__: 'formatHMS'
  };

console.log(`running in ${ dev ? 'development' : 'production' } mode`);

export default [

  {
    // ES6 output
    input,
    watch,

    plugins: [
      replace({
        ...tokens,
        __POLYFILL__: '' // no polyfill for ES6
      }),
      resolve({ browser: true }),
      commonjs()
    ],

    output: {
      file: './build/bundle.mjs',
      format: 'iife',
      sourcemap
    }

  },

  {
    // ES5 output
    input,
    watch,

    plugins: [
      replace({
        ...tokens,
        __POLYFILL__: "import './lib/polyfill.js';" // ES5 polyfill
      }),
      resolve({ browser: true }),
      commonjs(),
      buble()
    ],

    output: {
      file: './build/bundle.js',
      format: 'iife',
      sourcemap
    }

  }

];

Run npx rollup --config to build both the ES6 build/bundle.mjs and ES5 build/bundle.js scripts. The HTML file must be changed accordingly:

<script type="module" src="./build/bundle.mjs"></script>
<script nomodule src="./build/bundle.js" defer></script>

Modern browsers will load and run the ES6 contained in ./build/bundle.mjs. Older browsers will load and run the ES5 (plus polyfill) script contained in ./build/bundle.js.

Transpiling with Babel

Bublé is easier, faster, and less fussy, but Babel can be used if you require a specific option. Install the following plugins:

npm install @rollup/plugin-babel @babel/core @babel/preset-env --save-dev

Then include Babel in your configuration file:

import { getBabelOutputPlugin } from '@rollup/plugin-babel';

Then append this code to your plugins array:

    plugins: [
      getBabelOutputPlugin({
        presets: ['@babel/preset-env']
      })
    ],

The output.format must also be changed to es or cjs before running.

Minify Output

The fabulous Terser minifier can compact code by optimizing statements and removing whitespace, comments, and other unnecessary characters. The results can be dramatic. Even in this small example, the Rollup.js output (which has already created a smaller bundle) can be reduced by a further 60%.

Install the Rollup.js Terser plugin with:

npm install rollup-plugin-terser --save-dev

Then import it at the top of your Rollup.js configuration file:

import { terser } from 'rollup-plugin-terser';

Terser is an output plugin which is processed after Rollup.js has completed its primary bundling tasks. Therefore, terser() options are defined within a plugins array inside the output object. The final configuration file:

// Rollup.js configuration
import replace from '@rollup/plugin-replace';
import { nodeResolve as resolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import buble from '@rollup/plugin-buble';
import { terser } from 'rollup-plugin-terser';

// settings
const
  dev = (process.env.NODE_ENV !== 'production'),
  sourcemap = dev ? 'inline' : false,

  input = './src/main-replace.js',

  watch = { clearScreen: false },

  tokens = {
    __CLOCKSELECTOR__: '.clock',
    __CLOCKINTERVAL__: 1000,
    __CLOCKFORMAT__: 'formatHMS'
  };

console.log(`running in ${ dev ? 'development' : 'production' } mode`);

export default [

  {
    // ES6 output
    input,
    watch,

    plugins: [
      replace({
        ...tokens,
        __POLYFILL__: '' // no polyfill for ES6
      }),
      resolve({ browser: true }),
      commonjs()
    ],

    output: {
      file: './build/bundle.mjs',
      format: 'iife',
      sourcemap,
      plugins: [
        terser({
          ecma: 2018,
          mangle: { toplevel: true },
          compress: {
            module: true,
            toplevel: true,
            unsafe_arrows: true,
            drop_console: !dev,
            drop_debugger: !dev
          },
          output: { quote_style: 1 }
        })
      ]
    }

  },

  {
    // ES5 output
    input,
    watch,

    plugins: [
      replace({
        ...tokens,
        __POLYFILL__: "import './lib/polyfill.js';" // ES5 polyfill
      }),
      resolve({ browser: true }),
      commonjs(),
      buble()
    ],

    output: {
      file: './build/bundle.js',
      format: 'iife',
      sourcemap,
      plugins: [
        terser({
          ecma: 2015,
          mangle: { toplevel: true },
          compress: {
            toplevel: true,
            drop_console: !dev,
            drop_debugger: !dev
          },
          output: { quote_style: 1 }
        })
      ]
    }

  }

];

The Terser configuration differs for ES5 and ES6 primarily to target different editions of the ECMAScript standard. In both cases, console and debugger statements are removed when the NODE_ENV environment variable is set to production.

The final production build can therefore be created with:

npx rollup --config --environment NODE_ENV:production

The resulting file sizes:

  • ES6 ./build/bundle.mjs: 294 bytes from an original bundle of 766 bytes (62% reduction)
  • ES5 ./build/bundle.js: 485 bytes from an original bundle of 1,131 bytes (57% reduction)

Your Next Steps with Rollup.js

Few developers will need to venture beyond the command-line options above, but Rollup.js has a few other tricks …

Rollup.js JavaScript API

Bundling can be triggered from Node.js code using the Rollup.js JavaScript API. The API uses similar parameters to the configuration file so you can create an asynchronous function to handle bundling. This could be used within a Gulp.js task or any other process:

const rollup = require('rollup');

async function build() {

  // create a bundle
  const bundle = await rollup.rollup({
    // input options
  });

  // generate output code in-memory
  const { output } = await bundle.generate({
    // output options
  });

  // write bundle to disk
  await bundle.write({
    // output options
  });

  // finish
  await bundle.close();
}

// start build
build();

Alternatively, you can use a rollup.watch() function to trigger handler functions when source files are modified.

Create Your Own Rollup.js Plugins

Rollup.js offers many plugins, but you can also create your own. All plugins export a function which is called with plugin-specific options set in the Rollup.js configuration file. The function must return an object containing:

  1. a single name property
  2. a number of build hook functions, such as buildStart or buildEnd, which are called when specific bundling events occur, and/or
  3. a number of output generation hooks, such as renderStart or writeBundle, which are called after the bundle has been generated.

I recommend navigating to the GitHub repository of any plugin to examine how it works.

Rollup Revolution

Rollup.js takes a little while to set up, but the resulting configuration will be suitable for many of your projects. It’s ideal if you want a faster and more configurable JavaScript bundler.

Quick links:

Craig is a freelance UK web consultant who built his first page for IE2.0 in 1995. Since that time he's been advocating standards, accessibility, and best-practice HTML5 techniques. He's created enterprise specifications, websites and online applications for companies and organisations including the UK Parliament, the European Parliament, the Department of Energy & Climate Change, Microsoft, and more. He's written more than 1,000 articles for SitePoint and you can find him @craigbuckler.

New books out now!

Get practical advice to start your career in programming!


Master complex transitions, transformations and animations in CSS!

Latest Remote Jobs