I never thought too much about Javascript bundlers, I just used webpack. Recently I started switching to Svelte.js as my goto Javascript frontend “framework”. With Svelte I also used Rollup as default bundler, so it may be worth checking out the differences between the two and which one I might continue to use.

So that’s my project for today.

What is a Bundler anyways?

Definition

In the Javascript world, a bundler is a tool that takes all your Javascript files with any external dependencies, but also static assets and bundles those into a standalone web application you can deploy for production use.

It can also minimize your code, to improve filesize or add polyfills to support older browsers. (Please disappear, Internet Explorer. Please!)

Bundlers are also used in conjunction with Javascript Frameworks like Vue.js so you can use component files, like app.vue which then get transpiled into vanilla Javascript and HTML on production.

Then there’s Code Splitting, which splits up you code and lazily loads parts that are needed, only when they are needed, which speeds up the initial load time.

With any bundler you can use plugins which enhance their functionality, like preprocessing css with postcss.

Features I will look at in this comparison:

  • code splitting
  • minimization
  • hot reload

The (un-)real world example

For this example I did choose not to use any Javascript frontend framework, to keep it simple. But still I wanted a few features:

  1. hot reloading: I want my bundle to act as a development server that reloads my page evertime I make an edit
  2. minimization: the code the bundler produces should be as small as possible
  3. code splitting: I want my code file split, so it only loads minimal js at first and lazily loads the rest
  4. scss: I want to write scss code, which gets transpiled through the bundler

So let’s have a simple markdown editor as an example, that allows you to write markdown and get a live preview.

Let’s say we have initialized a npm project with npm init -y. (Example package.json)

With following file structure:

- dist
-- index.html
- src
-- index.js
-- main.scss
-- MarkdownParser.js

Imagine we have the following HTML (index.html):

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Bundler Comparison</title>
  </head>
  <body>
    <h1>Markdown Editor</h1>
    <input type="checkbox" id="md-live-preview" />
    <label for="md-live-preview">Live preview</label>
    <div id="md-wrapper">
      <textarea id="md-input"></textarea>
      <div id="md-preview"></div>
    </div>
    <script src="bundle.js"></script>
  </body>
</html>

And an index.js file like this:

import './main.scss';

const input = document.getElementById('md-input');
const preview = document.getElementById('md-preview');

let MarkdownParser = null;

const livepreview = async e => {
  MarkdownParser =
    MarkdownParser || (await import('./MarkdownParser')).MarkdownParser;
  const parser = MarkdownParser();
  parser.parse(e.target.value).to(preview);
};

document.getElementById('md-live-preview').addEventListener('click', e => {
  if (e.target.checked) {
    input.addEventListener('input', livepreview);
    livepreview({ target: input });
  } else {
    input.removeEventListener('input', livepreview);
  }
});

Which dynamically imports this MarkdownParser.js module.
More on dynamic imports: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#Dynamic_Imports

And a scss file with some basic css styles. main.scss

Find this complete example on Github: https://github.com/TimoBechtel/js-bundler-comparison-demo


Now we want to bundle this app with it’s dependencies (showdown), transpile our scss into css and load the showdown dependency when the live preview is activated.

Let’s go:

Webpack

Installation

We use npm to install webpack: npm i -D webpack webpack-cli Now if we run npx webpack (which runs ./node_modules/.bin/webpack) we see that webpack creates bundles our src/index.js in a dist/main.js.

To configure webpack, we can create a webpack.config.js file and define a name for the output file:

var path = require('path');

module.exports = {
  output: {
    filename: 'bundle.js'
  }
};

Code splitting

As you may have notices, webpack builds next to the bundle.js file two other files: 1.bundle.js and 2.bundle.js. These files include our MarkdownParser and showdown module and are only loaded when we click the “live preview” button.

Sass with webpack

To transpile our sass, we need a few plugins for webpack. I used following plugins:

  • css-loader: lets you import css files like import 'main.css;
  • style-loader: injects the imported css it into your HTML DOM
  • sass-loader: transpiles your imported sass into css with node-sass

npm i -D css-loader style-loader sass-loader node-sass

We can add a rule to use these loaders for our css/scss files: webpack.config.js:

[...]
module: {
    rules: [
      {
        test: /\.s[ac]ss$/i,
        use: [
          // Creates `style` nodes from JS strings
          'style-loader',
          // Translates CSS into CommonJS
          'css-loader',
          // Compiles Sass to CSS
          'sass-loader'
        ]
      }
    ]
  },
};

Live reloading with webpack

To serve our app app for development and also automatically reload it when we change something, we can use the webpack-dev-server: npm i -D webpack-dev-server

In our webpack.config.js:

  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    port: 9000
  }

We can now start our dev server with npx webpack-dev-server.
To make it a bit easier, we can add a script in our package.json file:

"scripts": {
  "dev:webpack": "webpack && webpack-dev-server",
},

And run npm run dev:webpack.

Hot module replacement

With HMR, your development server only updates those modules which you have changed. So you can change some code without your whole website reloading!

You can enable this with hot: true:

  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    compress: true,
    port: 9000,
    hot: true,
  }

Production

Add mode:'production' to your wepack config file, to build minimized production ready files. You may also want to use environment variables like mode: process.env.NODE_ENV || 'development'. Which you can set like this when building for production: export NODE_ENV=production or set NODE_ENV=production on windows.

Example config: https://github.com/TimoBechtel/js-bundler-comparison-demo/blob/main/webpack.config.js

Then you can run something like: export NODE_ENV=production; npx webpack or add script to your package.json.

Info

Don’t forget to set NODE_ENV to development, when adding new devDependencies with npm i -D, because otherwise it skips installing them!

Rollup

Installation

npm i -D rollup

Add a configuration file and specify our input and output files:

In Rollup you can use ES Module syntax 🎉

rollup.config.js

export default {
  input: {
    bundle: 'src/index.js'
  },
  output: {
    dir: 'dist/',
    format: 'es'
  }
};

To bundle npm modules we need the @rollup/plugin-node-resolve plugin: npm i -D @rollup/plugin-node-resolve

and npm i -D @rollup/plugin-commonjs to bundle common js modules.

Add it to our config file:

import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';

export default {
  [...]
  plugins: [commonjs(), resolve()]
};

Now we can run npx rollup -c -w to bundle our files and rebuild every time we change something.

To make it a little easier we might just add a command to our package.json:

[...]
"scripts": {
    "dev:webpack": "webpack && webpack-dev-server",
    "dev:rollup": "rollup -c -w",
},
[...]

And run it with npm run dev:rollup.

Code splitting

As you can see, we define a output directory instead of single file, because dynamic imports are automatically split into different files by Rollup.

You might want to change the script type to module, so your variables are not scoped globally. We can do this, because we set the output format to “es”:

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

That’s because we now build an es module. Checkout this starter project, on how to add support for older browsers with SystemJS: https://github.com/rollup/rollup-starter-code-splitting

Sass with Rollup

To import css I used the rollup-plugin-postcss plugin. Which also supports scss if node-sass is installed.

npm i -D rollup-plugin-postcss

and add it to the config:

import postcss from 'rollup-plugin-postcss';

export default {
  //...
  plugins: [
    //...
    postcss()
  ]
};

Live Reloading with Rollup

To set up a development server and reload whenever a file changes, we can use rollup-plugin-serve and rollup-plugin-livereload.

npm i -D rollup-plugin-serve rollup-plugin-livereload

and configure it in our rollup.config.js

import serve from 'rollup-plugin-serve';
import livereload from 'rollup-plugin-livereload';
// other imports ...

export default {
  //...
  plugins: [
    serve({
      port: 9000,
      contentBase: 'dist'
    }),
    livereload()
    // other plugins ...
  ]
};

As of now, Rollup does not support Hot Module Replacement. There is however this project, which seems to add some development features: https://github.com/PepsRyuu/nollup But I haven’t looked into this yet.

Production

To minify the bundle you can use rollup-plugin-terser:

npm i -D rollup-plugin-terser

import { terser } from 'rollup-plugin-terser';
//...
export default {
  plugins: [
    //...
    terser()
  ]
};

To only use plugins in production like terser you can conditionally load them like so:

const dev = process.env.ROLLUP_WATCH;
//...
plugins: [
  dev &&
    serve({
      port: 9000,
      contentBase: 'dist'
    }),
  dev && livereload(),
  commonjs(),
  resolve(),
  postcss(),
  !dev && terser()
];
//...

So it only activates some plugins when rollup is started in watch mode npx rollup -c -w, but not when building like npx rollup -c.


Example rollup config file: https://github.com/TimoBechtel/js-bundler-comparison-demo/blob/main/rollup.config.js




So there you have it. A quick setup for both bundlers. You can find the example repo here: https://github.com/TimoBechtel/js-bundler-comparison-demo

Links: