Keep An Eye On These Build Tools For Your Web Apps

pegahsafaie

Pegah Safaie

Posted on May 23, 2021

Keep An Eye On These Build Tools For Your Web Apps

Introduction

You might remember the battle in which webpack won over module loaders and task runners such as requireJS and gulp.
For a while, most of the webapps were built and bundled with webpack. Libraries on the other hand mostly preferred rollup as bundler.
But things are changing. There is a new generation of fast build tools that has a pretty good chance to keep up or even overtake Webpack. They might not be yet mature enough for production, but they are on the right path, so they are worth monitoring.

What are these fast build tools?

Vite, Snowpack and esbuild.

What makes them alternative for webpack?

  • They are fast
  • In what?
  • Build time
  • How?
  • The bundling is excluded from the build process of "Vite" and "Snowpack" and "esbuild" is being written in Go instead of javascript!
  • now I know the names. Why should I read on?
  • Because I want you to deeply understand how and why we need bundling and how and why we can exclude it from your build process! So stay with me if you are interested.

This would be a history lesson. But why?

When talking about a tool/concept in our ecosystem, it is important to keep in mind that these tools/concepts are created in response to the needs of their times. Therefore, to understand how a tool is built, we need to put it in the context of time and understand what problem it was supposed to solve at the time. By using this approach, you won't continue to believe in something just because your fathers did ;)
Here is the history of evolving JS module systems and their impact on task runners, module loaders, build tools and bundlers.

Problems of a world without having modules

Alt Text
The first javascript project I saw had a huge main.js file referenced by a script tag in index.html. Later some colleagues complained about the mess in this huge file, so we decided to write every single feature in one file and include all of them as script tags in index.html file.

We detected a problem very quickly: We were all working on different files and each had his own variables. In some cases, we could have used the same variable names in separate files.
Why was there a problem?

The variables inside a function in JavaScript are limited to that function, but everything outside of the function is global and accessible even from other scripts referenced in the same HTML page (that changed after introducing ES2015 and defining module and block scope besides function scope). Suppose everyone were to change the variable you depend on, you could easily run into conflict.

IFFE pattern, one step forward

Alt Text
The IIFE pattern was introduced to solve the name collision(encapsulation) problem we saw in the previous section.
By looking at the picture, we see that the authors of external.js and func1.js wrapped their code into an immediately executing function

(()=> {})()
Enter fullscreen mode Exit fullscreen mode

This function returns the functions and variables we want to export.
When our name collisions problem was resolved, we had some free time to start complaining about another issue in the non-module world: dependency management. Let me put it into the context:
-Dependency: An external code your code depends on. For example, func1.js is a dependency of main.js and external.js is a dependency of func1.js
-Management: Refrencing JS libraries (scripts) in an HTML file with a script tag.

Managing dependencies using script tags was a challenging task for three reasons:

  • The order in which scripts are included matters. The code above does not work if main.js is referenced after func1.js.
  • If your external library requires other dependencies, you must be aware of and specify all of their dependencies.
  • For the newer version, you must check the creator's website regularly and replace the old address in the index.html with the new one.

CJS modules, node and NPM. A revolution in dependency management

Alt Text
Nodejs was born in 2009 and revolutionized the modules world. As Nodejs applications don't have any html files in which dependencies can be defined, the Nodejs society has developed its own way of writing modular code, which turned out to be better than the IFFE pattern. The system was called CommonJS module system (CJS).

IFFE and CJS both support encapsulation, but CJS(+npm) has a more efficient way of handling dependencies. As an example, imagine you need to use lodash in one of your modules. Instead of placing <script src='./lib/lodash'></script> inside of index.html which makes it available and global for all your modules, you can place

const lodash = require('./lib/lodash');
Enter fullscreen mode Exit fullscreen mode

inside the js file you want to use lodash.

If you have already used the require syntax to import modules you might get confused by the relative address whitin require. what we write nowadays looks like:

const lodash = require('lodash');
Enter fullscreen mode Exit fullscreen mode

The difference lies in NPM(Node Package Manager) and your package.json file.
You can use the latter code by adding the lodash to your package.json file and then running the npm install command. NPM downloads the latest version of lodash(based on the semver rules you specifies for lodash) and put it in the node_modules folder. By writing require('lodash'), Node finds and reads the content of the "lodash" folder within node_modules.
CJS was(and continues to be) widely accepted in javascript ecosystem and many famous libraries like lodash are written using this system.

Nodejs also led to the emergence of Modejs task runners(2011) like Grunt and Gulp that were used alone or in conjunction with other build tools to manage our development pipelines and accomplish tasks like minifying and concatenation(both helped to reduce the amount of code you deploy and the others have to download). The field of build tools was dominated by them until 2015, when webpack arrived.

The problem with CJS is that it cannot be used in a browser! When you run npm install, NPM will still download lodash and place it under the lodash folder in node_modules. However, unlike node, browser does not know what to do with require('lodash').

AMD modules and module loaders

Alt Text

There were two main reasons why CJS was not suitable for browsers:

  • In CJS, only one module per file is allowed This increases the network traffic.
  • Dependencies(modules) can not be loaded asynchronously. require is a sync command. When you have ten require dependencies, they all will be loaded one after the other. Nodejs dos'nt have any issues with that since everything executing on the server and IO actions are executed really quickly. However, the browser will freeze until all 10 are downloaded. By comparison, an async module load sends ten requests at the same time (even when they are dependent on one another) and serves the user even while the dependencies are downloading.

To resolve these two issues, frontend developers devised AMD module which come with RequireJS as its module loader.
But what is a module loader for?

Do you remember how Node supported CJS to manage dependencies?
Well, no browser do it for AMD. That's why AMD needs its own dependency manager which loads as a script in the browser and performs the exact same functions as node did for CJS.
AMD Module loaders manage dependencies for AMD module systems in an async manner! Lets see an example to understand it better.

Main.js depends on two dependencies, A and B. RequireJS loads B and then A asynchronously(at the sametime) but, it does not allow main.js to be executed until both of them are completely loaded.
Alt Text

AMD and Requirejs combination was popular for several years, but did not last because of two reasons:
The syntax of AMD modules was confusing for exporting and importing modules. Further, javascript developers were increasingly using CJS libraries written primarily in Nodejs, such as lodash.

CJS modules, browser and bundlers

Alt Text
To solve CJS's "sync module load" problem for browsers, some smart developers jumped in and created a build tool named Browserify(2011). Browserify crawls the whole webapp at build time to find all requires and replaces them with content from the node-module folder(since it runs in build time). At the end of the build we have one single js file containing all the js files.
Alt Text
Browserify allowed frontend developers to write their entire webapp in CJS module format.

World of incompatible modules

At this point, developers can choose from a variety of modules and write their projects in one of them. The problem arises when one tries to use a module from another system but the formats are incompatible.
In order to resolve this problem, two solutions were proposed:

  1. Some developers preferred to create a standard module system that works with both nodejs(backend) and browsers(frontend). They introduced the ES module system(2016) but it was not supported by neither Nodejs nor browsers until 2019.
  2. Some other developers proposed to create a bundler that accepts all types of modules. You probably know it: webpack. Webpack is a swiss army knife which allows developers to write code in multiple module systems (for example, ES and CJS simultaneously). It also provides additional functionalities like asset management, code splitting, tree shaking, etc.

Alt Text

In this phase, we have modules which can support encapsulation and provide dependency management, and thanks to webpack we can use our favorite libraries without being worry of incompatibilities. Are we at the end of our journey?
The answer is no. JS world prefers to have a native solution for this incompatibility problem. Wouldn't it be better to have one standard module that doesn't need any extra transpilation system to be understood by browsers and nodes? Remember that webpack's nice features came with a price: complexity. Webpack is difficult to configure even after starting with a zero configuration at version 4.

Supporting ES modules and new bundlers

After node v13 and some famouse modern browsers started to support ES modules, more and more developers from both environments started to use ES modules. Even famouse CJS modules like lodash delivered an ES version of their library.
In addition to its standard status, ES is popular for two other reasons:

  1. Its static nature allows for some build-time bundle optimization, such as tree shaking.
  2. The flexibility to define multiple modules in one file (AMD managed to do this, but their implementation had a number of other problems).

It was suggested that since everyone writes their code in ES modules and node and most browsers can resolve es modules, why would we use a complicated bundler like webpack to convert each module type into another? What if we just had a bundler for ES modules?

This led to the creation of Rollup. The philosophy was:
Since ES modules can be deployed both in browsers and in nodes, this should eliminate the need for unnecessary transformations. Using this approach, Rollup could reduce not only the configuration complexity but also the bundle size as well as the build time.
Alt Text
There is an old saying, "Use Rollup for your libraries and Webpack for your web applications". It's a rule of thumb, and many web apps are bundled with Rollup or vice versa, but what makes Webpack the most appealing choice for building webApps?

  1. Asset(css, images, font) management features
  2. Giving developers the flexibility to write their code in the ES modules while importing the CJS libraries like lodash(It is also possible with Rollup but requires plugins).

However, Rollup is popular among libraries since it supports a variety of expose formats, including ES, CJS, AMD, and script tags (whereas webpack does not support ES output).

Supported ES modules and non-bundlers

After modern browsers began supporting ES modules, a discussion began between two groups of developers:

Revolutionary developers: Currently, our development team's productivity is negatively affected by bundling. During build time, we spent too much time finding and resolving dependencies. Why not just transpile other popular module formats(CJS) into ES and let the browser resolve and execute them?
Alt Text

Conservative developers: No bundle?! You will have a network request per file which effects your runtime performance.
And would you be able to guarantee that your clients do not use old browsers like IE11(which does not support ES modules)?

Revolutionary developers: We can't guarantee which browsers our clients use, so we still bundle for our production mode. However, we can ensure that our developers have access to modern browsers so that we can skip bundling in development mode. This is what our proposal looks like:

if (production) {
    // Do bundling
    // Do the rest of building tasks
}
else {
    // Do the rest of building tasks
}
Enter fullscreen mode Exit fullscreen mode

Conservative developers: ok! Let's give it a try. But we will be careful with that because due to the different build approaches what you see and test in development mode could be different from what you have finally in your production.

Vite(pronounced /vit/) and Snowpack were created as a result of this discussion. There are many similarities between them. The big difference is that vite is opinionated. It means you get rid of some configuration but it also reduces your flexibility.

Alt Text

For example Using Vite, your production will use Rollup to bundle, whereas with Snowpack, you can use your choice of bundle (or even leave it unbundled).

Generally, if you're comfortable with tools such as vue-cli or react-create-app, which set up scafffolding projects with zero config, Vite would be a good choice for you.

It seems interesting for me as a Vue developer who does not like to set up a project from scratch with its build tool. But before using Vite for development, you should be aware of some aspects.

Until recently, Vite had no official support for legacy browsers. To put it simply: Vite's created bundles didn't work with IE11. Currently, it is supported by an official plugin named: plugin-legacy.
Alt Text
Another fact to know is that using Vite you can import CJS dependencies in ES modules, but in some cases, it may not work quite as expected.

esbuild

from esbuild website. the time to do a production bundle of 10 copies of the three.js library from scratch using default settings, including minification and source maps

Esbuild is an impressively fast bundler based on Go, which many think will be the most used bundler for big applications in the future. esbuid's problem is its small community team and the fact that it is not production-ready. Some features like dev server is missing but you can use the combination of Snowpack and esbuild.
Furthermore, Vite documentation indicates that esbuild would be preferred over or in addition to Rollup when esbuild became production ready.

Alt Text

History overview

Alt Text

Request

I am so interested to see which module system and build tool you use/have used for your projects. Drop a comment below and let's talk about it.

Resources

Unbundling the JavaScript Module Bundler

Writing Modular JavaScript With AMD, CommonJS & ES Harmony

Java Script Module Systems Showdown

Moving Past RequireJS

Why AMD

Comparing the New Generation of Build Tools

Why I Use Rollup And Not Webpack

Modern JavaScript Explained for Dinosaurs

💖 💪 🙅 🚩
pegahsafaie
Pegah Safaie

Posted on May 23, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related