JavaScript Bundlers: An in-depth comparative 👍👎 Is Webpack still the best bundler in 2021? 📦
Tags: javascript, webpack, webdev, productivity
Published: 2021-07-09 15:15:54

Hello, everybody! 🚀

For the last few days, I've been doing some research on the currently available __JavaScript bundlers__ to try to draw my own conclusions about them and figure out which one would be more appropriate for my projects. And, of course, to find out if it's all about popularity and we developers are overrating some of them and underrating the others 😇

Since the only bundler I've been working with for the last few years is __Webpack__, I decided to take a look at __npm trends__ to find out __which the most popular JS bundlers are in 2021__ and give them a try.

And this is what I got: 

![Comparative chart from npm-trends showing the top 5 JS bundlers](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9ispe7ysyot47gsnid67.png)

So today, we'll be making a comparison between the __5 most popular bundlers__ according to ___npm trends___: Webpack, Rollup, Browserify, ESbuild and Parcel.


In this comparative, we will create a __really basic scenario__ for each of them with a couple of the most used resources/tools these days, and we'll be talking about their __pros and cons__ and comparing them all based on __a few parameters__.


#First things first. What is a ___bundler___? 🤔
A __bundler__ is a tool that __puts together all your JavaScript code and its dependencies__ and throws a __new JavaScript output file__ with everything merged, ready for the web, commonly known as _the bundle file_.

These _bundlers_ can work with __other types of files__ as well apart from JavaScript, but ___they need a little help___ to perform their _bundles_. We'll talk about this more in depth in each of the examples below.

None of them require a _config_ file, what perfectly works for the most basic bundle. This means you have a _.js_ file converted into another _.js_ file with minimal setup. But, once you start having __more and more kinds of files that need to be transpiled__ and, consequently, configured, it's time to add a ___config___ __file__ because, otherwise, you'll find yourself immersed in __chaos__ 😰
#The scenario 🖥

To try out these bundlers, __we don't need a specific complex structure__ for our project, so let's propose a really basic scenario: __an HTML file__, with __some styles__ (we'll slightly complicate it by using a __preprocessor__ like SASS) and __ready to use ES6__, which means we will include _Babel_ even though we're not using React, Vue or any library/framework that rely on it in this comparative. But let's get it setup anyway.
```
/dist
   bundle.js
   [styles.css]
/src
   index.js
/styles
   styles.scss
index.html
package.json
[*.config.js]
```
__/dist__ will be __the folder created after the bundle process__ and will __contain all the bundled files__. The bundled file for the styles is __optional__ because we can choose either to inject the styles directly in the HTML or generate a new _transpiled_ file containing the styles.

__/src__ is the folder containing the __entry point__ from which the bundler will __start the bundle process__.

__/styles__ is the folder containing the __original styles file__, before the bundle.

__index.html__ is the file containing what we'll see in the __browser__.

__package.json__ is the file where all the __dependencies__, __scripts__ and __some configurations__ are stored.

__*.config.js__ is the file where all the __config for the bundler__ is defined. This file is __optional__ for every bundler in this list, __but highly recommended__. * will be replaced accordingly by the name of the bundler.

____

Having said all this, let's see what each of these 5 bundlers can offer us.

#1. Webpack
Loved by many, hated by some, known to all. And still __the most popular bundler in 2021__. With __more than 15 million weekly downloads__ (at the time of writing this post), there's no doubt that __Webpack is still the bundler__ ___par excellence___ in 2021. But, is it the easiest to use, configure and understand how it works?

Let's have a look at how we should configure it to have it ready to work.

##Approach used by Webpack
- A _build_ script
- A _config_ file
- _Loaders_ used to _transform_ files
- _Plugins_ for more complex stuff

`package.json`
```json
...
"scripts": {
   "build": "rm -rf dist && webpack --mode development"
}
...
```
Really really easy. There's no need to do anything else for a basic configuration. Actually, if you don't want to use a different name for your configuration file, you don't even need to specify a configuration in the build script. If you want to use a different one, you should add _--config your_config_file.js_ to the command.

>Note that we'll be adding the command _rm -rf dist_ to every build of every bundler. What this does is removing the _dist_ folder every time a new _build_ script is executed.

`webpack.config.js`
```js
module.exports = {
   entry: "./src/index.js",
   output: {
      filename: "bundle.js",
      path: path.resolve("dist")
   },
   module: {
      rules: [
         {
            test: /\.(js|jsx)$/,
            exclude: "/node-modules/",
            use: "babel-loader"
            
         },
         {
            test: /\.html$/,
            use: "html-loader"
         },
         {
            test: /\.(scss|sass)$/,
            use: ["style-loader", "css-loader", "sass-loader"]
         }
      ]
   }

}
```

Maybe a little bit more tricky and difficult to understand at first than the other bundlers, but really __easy once you get the sense of how everything works together__.

##What is happening in this _config_ file? 🙃

Well, first, we need an __entry point__ for our bundler to start merging everything. That is specified in the _entry_ attribute and the file will be our file _index.js_ in the folder _src_.

Same for the __output file__, we'll tell Webpack our file will be called _bundle.js_ and it should be stored in the folder _dist_.

And now, what's only left is to __handle the files that are not JavaScript (ES5)__. Webpack takes care of these files using __loaders__. To _transform_ these files, we just need to indicate the file format and which loader(s) will deal with them.

So that's what we need: a few loaders to take care of our styles, our HTML and our JS (ES6 - remember that we're getting it ready for formats like _.jsx_): `style-loader`, `css-loader` and `sass-loader` for the styles, `html-loader` for the HTML files and `babel-loader` for ES6.

Notice that we're also _transforming_ the HTML file (this loader will be useful if we want to add resources that are loaded directly in the HTML file, such as images). This loader is really useful in bigger projects, but not necessary in this case (due to its simple structure), we'll skip this step for the rest of the bundlers.

And this is it. Everything will be bundled once we run the _build_ command.

##About the bundle
Since we're using `style-loader` to bundle the styles, instead of a plugin to minify CSS and generate a new file (`MiniCSSExtractPlugin`), the styles are injected into the HTML file inside a `<script>` tag, so the only output file is `bundle.js`, which needs to be added to `index.html`.

##What do I think about Webpack? 👇
I have to admit that first time I had to face Webpack I thought the configuration would be impossible. It was my first time using a bundler and I was barely able to understand the overall concept. Let alone all the loaders and more complex related stuff because it was a bigger project.

But after a few from-scratch configurations on my part, I have to say that now __I find it more intuitive and easier to set up__ if I compare it to what it felt like to get to know the rest of them.

Let's take a look at the others and you'll understand why! 

#2. Rollup

Let's now turn our attention to __Rollup__. As well as the rest of the loaders, this has been my first time trying it out, so I'll also provide my first impressions about it 🤓

##Approach used by Rollup

- A _build_ command.
- An _optional_ _config_ file.
- _Plugins_ used to _transform_ files

`package.json`

Basic bundle with no _config_ file:

```json
...
"scripts": {
   "build": "rm -rf dist && rollup src/index.js --file dist/bundle.js"
}
...
```

Using a _config_ file:

```json
...
"scripts": {
   "build": "rm -rf dist && rollup -c"
}
...
```

A really easy command for building, as well, so nothing else to point out here.

Let's now check the _config_ file, that is __optional__ but __recommended__.

`rollup.config.js`
```js
import babel from "@rollup/plugin-babel";
import scss from "rollup-plugin-scss";

export default {
   input: "./src/index.js",
   output: {
      file: "./dist/bundle.js",
      format: "cjs",
   },
   plugins: [
      babel({ exclude: "node_modules/**" }),
      scss({ output: "styles.css" }),
   ]
}
```
What Webpack defines as _loaders_, here in Rollup are called just _plugins_. This time we just need a couple of them: the one for transpiling ES6 into ES5 (Babel) and the one for SCSS: `@rollup/plugin-babel` and `rollup-plugin-scss`.

These plugins have also __their own optional configuration__. In this case, for Babel, we're excluding the folder _node_modules_ and for SCSS we're giving the output file a different name. Otherwise, it will remain _output.css_.

For configurations with __plain CSS__, there's a plugin called `rollup-plugin-css-only` that works in the exact same way as the plugin we're using for SCSS.

Note that we need to specify the entry and the output points exactly as we did before with Webpack. 

And that would be it.

##About the bundle
The Rollup bundle comprises two files: `bundle.js` and `styles.css`. It's necessary to import the original styles files in the entry point `index.js` for the bundler to be able to find the file (there's no other place where we can reference it).

Also both _bundles_ need to be added to the HTML index file.

##My first impressions about Rollup 👇
To be honest, I wasn't expecting much of these other _easier slash light-weight_ bundlers since Webpack has always worked for me, and I have to say that Rollup has surprised me in a good way.

I find it __pretty similar with Webpack__ (_config_ file with almost the same structure, _plugins_ work in the same way as _loaders_ to translate _no-js_ files, the easy build command...), which means familiarity, usage recall and, consequently,  ease of use. 

The __only drawback__ I've been able to find so far is __the large number of dependencies it relies on__, and consequently, the __huge size__ of the project (3x a project bundled with Webpack). We'll be focusing on this more in depth at the end of the post 🔜

#3. Browserify

Let's now talk about __Browserify__. 

##Approach used by Browserify
- No _config_ file
- _Transforms_ used to _transform_ files
- Everything you need to configure -> `package.json`

The method used by Browserify has nothing to do with the _traditional_ approach of a _build_ command and a _config_ file. With this bundler, __every possible configuration__ is allocated in `package.json` and __the build command can get a little bit tedious__ if we don't have the concepts clear.

It also needs __plugins__ (or _transforms_, as they are also called) to _transform_ everything into something _readable_ by the browser.

Let's have a glance at how we can configure it:

`package.json`

```json
...
"scripts": {
   "build": "rm -rf dist && browserify -o dist/bundle.js src/index.js"
}
...
```
A very basic use of this bundler could be something like the code snippet above. We only have defined the input and output files (no configuration for styles or anything more complex).

Note the __length of the build command__ having only declared the input source and the output.

Let me show you how it would look like if we add the suitable plugin for handling plain CSS.

```json
...
"scripts": {
   "build": "rm -rf dist && browserify -t [browserify-css --output dist/styles.css] -o dist/bundle.js src/index.js"
}
...
```
Then, if we wanted to add some configuration to the plugin, we would do something like the following down below in the same file:

```json
...
"browserify": {
   "browserify-css": {
      "autoInject": true,
      "minify": true,
      "rootDir": "."
   }
}
...
```
It starts getting not _that maintainable_.

And now, let's complicate it a little bit more by adding plugins for SCSS and Babel. We need a couple of _plugins_ called `Babelify` and `scssify`.

>###Something to take into account
>I've been trying out this bundler with __the last released version of Node__ (v16.4.2) and the command line throws multiple errors when trying to install any dependencies that rely on `node-sass` (`scssify` and `sassify`, more specifically). A very negative point.

We could do this in two different ways: 
- By saturating the build script with more content 😅
- By adding a _transform_ property

###Via the build script

For specifying several _transforms_ in the _build_ script using Browserify, we should add as many as _-t_ [ _transform options_ ] as needed, like this:

```json
...
"scripts": {
   "build": "rm -rf dist && browserify -t [ scssify --output dist/styles.css ] -t [ babelify --presets [ @babel/preset-env ] ] -o dist/bundle.js src/index.js"
}
...

```

If you're using this method, pay close attention to the __white spaces__ inside the arrays. __They matter__ ✌️


I find this method __tedious__ and __difficult to understand__, and above all, __difficult to maintain__. And we're only using two plugins. All said.

###Via the transform property


```json
...
"browserify": {
    "transform": [
      [ "babelify", {
            "presets": [
               "@babel/preset-env"
            ]
        }
      ],
      [ "scssify", { "autoInject": true } ]
    ]
  }
...
```
By using this method, the _build_ script will look like it was originally, when it just performed the simple bundle of the input js file:

```json
...
"scripts": {
   "build": "rm -rf dist && browserify -o dist/bundle.js src/index.js"
}
...
```
Much better 😊

##About the bundle

The Browserify bundle consists of the `bundle.js` file and, only __if we set an output file for the styles__ in the plugin that takes care of them, __we'll get a__ ___styles.css___ file. __Otherwise__, the styles will be injected at the bottom of the `<head>` element in the HTML file __inside a `<script>` element__.

Take a look at this two different config examples for `browserify-css`: 

```json
...
[ "browserify-css", {
      "autoInject": false,
      "minify": true,
      "rootDir": ".",
      "output": "dist/styles.css"
   }
]
...
```
This config above will create a separate _.css_ file.
```json
...
[ "browserify-css", {
      "autoInject": true,
      "minify": true,
      "rootDir": "."
   }
]
...
```
And this other config will inject the code into a `<script>` tag in the head of `index.html`.

##My first impressions about Browserify 👇
My less favorite so far. __I don't find it__ as __intuitive__ as the other two, and the __approach__ it uses is totally __different__ from what we are _regularly_ used to. Also, I think the __configuration is more tedious__ if at first you don't know how and where to handle the required plugins.

Also, __blank spaces matter__, and if you don't know that beforehand, you can perfectly spend 2 hours trying to figure out what's wrong with your code 👎

#4. ESBuild

Time to talk about __ESBuild__.

##Approach used by ESBuild

- A _build_ command (encourages the use of the terminal)
- An _optional_ _config_ file
- _Plugins_ used to transform files

With ESBuild you can use __either the command line or a__ ___config___ __file__ as well as the others, even for more complex configurations. It's totally up to you, but specifying a _config_ file is always recommended for __maintainability, scalability, readability and productivity__ reasons.

We're going to create a _config_ file called `esbuild.config.js` and we'll execute it from the _build_ script by running the command `node`.

But first, let's have a look at the simplest way to start bundling our files with ESBuild (no _config_ file is required this time):

`package.json`
```json
...
"scripts": {
   "build": "rm -rf dist && esbuild --bundle src/index.js --outfile=dist/bundle.js"
}
...
```
As usual, we declare the entry point and the output file. And that's it. But what happens when we need to __keep bundling more different kind of files__?

Let's then take a look at the following example:

```json
...
"scripts": {
   "build": "rm -rf dist && esbuild --bundle src/index.js --outfile=dist/bundle.js && esbuild --bundle styles/styles.css --outfile=dist/bundle.css"
}
...
```
We are now bundling the styles as well, and adding a little more information to the _build_ script (mess alert again!) by defining two different bundlers. We could (and definitely will) have more filetypes that would need to get bundled and this could become a total mess.

So, let's put aside this approach and let's create a _config_ file.

`esbuild.config.js`

```js
import esbuild from 'esbuild';
import { sassPlugin } from "esbuild-sass-plugin";
import babel from 'esbuild-plugin-babel';

esbuild.build({
   entryPoints: ['src/index.js'],
   bundle: true,
   outfile: 'dist/bundle.js',
   plugins: [sassPlugin(), babel()],
}).catch(() => process.exit(1));
```
And here comes what I found (let me say) _weird_ and what took me some time to figure out.

Maybe because I was expecting to run this _config_ file in the same way as Webpack and Rollup do (they run their _config_ file by default if it exists and has the default name), I had some trouble trying to tell ESBuild to take it as an input for configuration.

Finally, I realized that __it should be called via the node command__ to just run the script 😬

```json
"scripts": {
   "build": "rm -rf dist && node esbuild.config.js"
}
```
And that was all. 
___

Something I want to mention here is that, the fact that __there aren't so many plugins from where to pick__ and also __most of them are way outdated__, doesn't make me particularly happy. And, if you allow me some advice, __try to pick plugins which use either CommonJS__ (which inserts modules through _require_) __or ES Modules__ (which does the same using _import_),  because if you mix them up... the only things you'll get will be errors and mess everywhere! 😖

Just make sure you __change the type attribute__ in `package.json` __if you're using ES Modules__ (_import_) to load your plugins into the _config_ file:

```json
...
"type": "module"
...
```

Practically all plugins have been created by the community (if not all). For this example, I've chosen `esbuild-sass-plugin` for SASS/SCSS and `esbuild-plugin-babel` for Babel. Both of them work with _import_, so no extra problems.

A great point to mention: ESBuild is really __fast compared with the others__. At least in this scenario.

##My first impressions about ESBuild 👇
Mixed emotions. At first, I thought it would be very easy to configure (it is if you only intend to perform a regular bundle) but then I started to __struggle a bit__ with the _config_ file, __not because of the syntax__ but because of the __multiple errors thrown__ on the terminal __regarding Node__. 

#5. Parcel
Let's now have a look at the last bundler in this list: the _famous_ Parcel. Hi to the huge community of Parcel fans out there 👋

##Approach used by Parcel

The Parcel approach is mainly based on a ___zero___ __configuration__ environment 😱 I was reluctant to believe it at first (that's the main reason why I wanted to try it out so bad), but, yes, it's possible to bundle a project like the one we're testing in this post by writing the bare minimum configuration, in a few minutes and without racking your brains 🙌

##Zero configuration? Are you sure? 😪
By zero they mean __very little and precise__. Let me show you the configuration I used for this basic project:

`package.json`

```json
...
"scripts": {
   "build": "rm -rf dist && rm -rf  && parcel build src/index.js --no-scope-hoist --no-source-maps"
}
...
```
The procedure is pretty much the same: we need to indicate where the __entry point__ for our app is located. And I also added the flags `--no-scope-hoist` __to avoid odd behaviors__ regarding `require` when running _js_ scripts and `--no-source-maps` __to avoid the creation of__ ___sourcemaps___. Otherwise, Parcel will create one for every bundle file by default.

Now, if we want __to change the location and the name of the output__ bundle file, we need to change the value of the _main_ property attribute in `package.json`, like this:

```json
...
"main": "dist/bundle.js"
...
```

Otherwise, the bundle will be generated at root level and will be called with the name that is stored in _main_, in most cases _index.js_ (if we didn't change it when running `npm init`).

##Now, let's _(zero)configure_ the styles and Babel
Since we're using SCSS, we need to use SASS as a preprocessor. So, what was my surprise when I read that __SASS is already included with Parcel installation__. But not only _SASS_, also _LESS_, _Stylus_, and... _Babel_! 😧

So the only step to take here is to create a couple of config files for SASS and Babel.

Our SASS config file will be named `.sassrc` and will contain the following code inside:
```js
{
  "includePaths": ["node_modules"]
}
```
And when we run the _build_ command, Parcel will automatically install the plugin `@parcel/transformer-sass` as a dependency and will create a `bundle.css` file in the same specified directory for the bundle, and that's all the configuration. Pretty cool, right? 

Now don't forget to link this file to your HTML 🤗  And remember that your _.scss_ file should has been previously __imported on your entry point__ file in order for the bundler to know what file it has to transform.

On Babel side, we need to create a `.babelrc`config file to specify the needed presets (let's say we want to get it ready for using React in the future): 

```js
{
   "presets": ["@babel/preset-env", "@babel/preset-react"]
}
```
Parcel will automatically call `@parcel/transformer-babel` and will do the job for us.

Don't forget to previously install `@babel/preset-env`, `@babel/preset-react` and all the dependencies needed by React. 

And that's... it. We're all set and ready to rock 😁 

##My first impressions about Parcel 👇
The first thing I want to point out is that Parcel wasn't that easy for me at the beginning since I had a really (really) hard time trying to get it ready to work, and it seemed like it wouldn't stop throwing errors regarding the OS and creating more trouble out of outdated versions of some dependencies 😥 So, to be honest, Parcel wasn't going to be in this list because I didn't want to talk about it if I couldn't try it out myself. 

But, _magically_ ✨ (and due to my tireless perseverance 😅), I finally could make it and set everything up to get it ready 🙌

And after that, it was really easy compared to the rest of the bundlers. So let's draw a veil over the setbacks and let's give it a chance.

Parcel is also pretty __fast__, because it uses ___cache___.

But... something I don't like at all is the __several security vulnerabilities that appear after installing it__ (around 12, some of them high risk) 😖 That doesn't speak well of you, Parcel. Not to mention the __huge size of the project__. __The heaviest__ in this comparative.

#The comparison 📈

Here you have the highlights of this comparative summed up in a table:

![Table that compares different features of the 5 mentioned bundlers](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/naipfc68z6u3looyvidm.png)

#What is my final verdict? 😼
Well, I think some of __these other bundlers apart from Webpack can be cool to use for small or side projects__, but in reality, I personally think that __Webpack__ is still the __best option for robust projects__ (we just have to look at the huge number of downloads per week compared to the rest). 

Also, I find it the __easiest to manage__ since once you get the sense of how it deals with configuration, it's easier to keep adding values to that configuration. But it's not something that obvious. You have to take your time playing with it to get a very basic idea at first.

Moreover, you have the majority of resources you need (loaders, plugins...) available __from the creators__, so you make sure you're using __a real source of truth__. And they are updated really frequently, so you can use it without worry with newer versions of Node and other packages.

So, yes, __I will keep choosing Webpack__ as my first option over the others.

___

My second choice would be __Rollup__ for sure, and I truly think __I will definitely use it in some of my side projects__ since I found it __intuitive to configure__ and it __seems like it works properly on robust projects__ as well. 

And about their _plugins_, most of them are also available __from the creators__, so, again, a real source of truth and many more advantages.

___

I also think __Parcel__ is __a very interesting option__ and I'd like to try it with larger projects and check if it really doesn't need further configuration. Definitely a great find.

And a big plus to the fact that __plugins__ like _Babel_, _SASS_, _LESS_ and some more are __built-in and ready to use__ out of the box.
___

What about __Browserify__ and __ESBuild__?

These two have been __the ones with which I have struggled more__, especially Browserify. The fact that it doesn't require a _config_ file and everything should be declared in _package.json_ kinda forces you to __change the way you think of how bundlers__ are ___traditionally___ __configured__. Not to mention you end up _saturating_ the file with __way a lot tricky configurations__, which makes it __difficult to read and maintain__. 

Also, when it comes to _plugins_, __most of them are not developed and maintained by the creators__ (especially the most common) and are __really outdated__ (many of them haven't been updated in the last 4 years) and this fact __leads to problems with newer Node/other packages versions__ and compatibility in general.

____

And on __ESBuild__ side, I didn't especially like it either. The first impression was good but then, since the _config_ file caused me some trouble, it mainly __ended up in confusion about how to manage configuration__ with and without this file. So I found it quite __ambiguous__ and took me a bit to realize __how to set up both scenarios__ in different ways.

About their _plugins_, same as Browserify, __practically all of them has been created by the community__, not the author, so you have to use them at your own risk. But as a plus point, __they are often updated and maintained__.

#Now it's your turn! 🔥
What do you think of this comparative? Do you agree? Which one is your preferred bundler? Do you know some other bundler that is not on the list? Would you like to suggest different bundlers for future comparative posts? Comment below!

#Disclaimer 👇
Remember this is just a post showing __my impressions about something I tried for the first time__. I have decided to share the process with you and my opinion about what I experienced. __The opinions expressed in this post don't mean that some bundlers are better over others__. My advice is to try them all out and draw your own conclusions, like I did. And based on that, use the ones you like best and fit your needs.

____

🎉 Don't forget to follow @underscorecode on [Instagram](https://instagram.com/underscorecode) and [Twitter](https://twitter.com/underscorecode) for more daily webdev content 🖥🖤

____

####And last but not least... A quick friendly reminder before we go 😊
We all know there are million ways to get things done when it comes to programming and development, and we're here to __help and learn__, so, if you know another possible way to do what others are sharing (__not better, not worse, just different__), feel free to share it if you feel like it, but, please, __always be kind and respectful__ with the author and the rest of the community. Thank you and happy coding!