When I first began learning about web development in 2007, I had never heard of compiling JavaScript before deploying it to websites. The idea of JavaScript modules had not been formally introduced. At the time JavaScript was only about a decade old and the Chrome browser wouldn’t come out for another year. Those were simpler days when web developers wrote directly for browsers.
In the early 2000s it was common practice to write just one main script for an entire website. Dependencies were in separate files that had to load before the main script, such as jQuery (released in early 2006) for handling cross-browser differences. It was normal to see inline scripts in <head>
or at the end of </body>
, as well. There were quite a few problems with this approach, such as polluting the global scope, but it worked for the low complexity of web applications at the time.
The beginnings of JavaScript Modules
As the demand for web applications rose, developers needed a better way to build and maintain online software at scale. As more and more features were required, the hassle of tracking and loading dependencies in the right order became too much of a burden. The increasing complexity often resulted in features being frustratingly intermingled and scattered in code. The solution—split your codebase into modules. Modules are encapsulated units of JavaScript linked by an export/import syntax.
CommonJS Modules to the rescue
Before modules would be supported by browsers, CommonJS introduced the concept starting in 2009. The specification describes loading modules in order into a file that could then be used by a browser. This requires a server which is capable of compiling JavaScript. (By the way, compiling JavaScript to “bundle” the JavaScript is not to be confused with something like CoffeeScript, which is another language that gets compiled to JavaScript.)
There were multiple implementations of CommonJS, the most prominent of which was Node.js, also started in 2009. Node.js pioneered the CommonJS ethos, which became a central component of “universal” JavaScript. Node.js not only processes JavaScript, it runs it—empowering modules to be written in JavaScript with no extra setup.
JavaScript Modules become standard with ES6
By 2011 JavaScript had broken out of the browser and could be used as a server-side scripting language. Entire applications were—and are—written in JavaScript, and share modules between client and server.
Today, native modules are a browser standard with ES6 (ECMAScript 2015+). ECMAScript Modules (ESM) work in much the same way as CommonJS Modules. Spanning the gap between ES5 and ES6 required tools that converted, “transpiled”, ES6 code to serve it to browsers that only supported ES5. These transpilers are often also compilers which process JavaScript, or are used by one. The most well-known example of a compiler with a transpiler is Webpack paired with Babel. (Most developers just refer to transpilers as compilers or “bundlers”.)
Most web developers in 2020 write in the latest version of ECMAScript that is supported by transpilers, rather than wait for browser support. The compiler combines modules into a “bundle”, then transpiles the code to another ECMAScript syntax. This is often called “code bundling” or “JavaScript bundling.” This allows developers to write a future-proof main script for a website or web application without any of the downsides of the past.
What is JavaScript code bundling?
Modern JavaScript for web applications can now be thought of as a bundle of modules interacting with each other to create features. Dependencies are modules, too, which means they no longer need to be loaded individually before the main file can load. The bundle is the main script file, which the browser loads through a <script>
tag. (Or there can be multiple bundle files, but more on that later.)
Minifying JavaScript and debugging with sourcemaps
There are also optimizations that can be done while bundling JavaScript. Once a bundle is created it is meant for the browser to consume and does not have to be human-readable. Minifying the bundle to remove formatting, such as white-space or long variable names, can reduce file size significantly.
However, if the code is not human-readable it won’t be easy to debug in the browser. That’s the reason sourcemaps were created, first seen with Google’s Closure Compiler. Sourcemaps act like decoders and enable browsers to parse the minified code and put formatting back in. They also contain references to the lines in the original files, which means it’s possible to track bugs to the exact source. Bundler tools offer an option to create a sourcemap file at the time of minifying, which is co-located with your bundle.
How does bundling relate to code splitting?
Of course, bundling often means an increase in file size. Since website loading time is a prime concern for users, it’s important to keep JavaScript bundles as small as possible. One method for reducing bundle size is “code splitting,” or creating multiple bundles from one codebase. All this requires is to set up entry points in the bundler configuration. Each entry is treated by the bundler as the starting point to recursively gather all required files being imported.
Code splitting to create multiple scripts
The most common reason to have multiple scripts for a single application is to take advantage of browser caching, which stores the file for later use and doesn’t require another load request to the server. For example, a vendor bundle with dependencies that are unlikely to change, such as utility libraries for math or date calculations. That way even when the application changes a returning user won’t need to load the unchanged vendor bundle.
This may seem backwards: modular JavaScript became standard and popular because it doesn’t require loading dependencies before the main script. The key difference is that these dependencies only include what the application is directly using. Even better, all of the dependencies are managed by the bundler.
There can also be a bundle for each page of a website. Or, bundles for each application that shares a code base. This is how modular JavaScript shows its full power—reusable code that can be used consistently throughout the code base. Features can be developed once and work consistently between server and client.
Tree shaking to help reduce bloat
When a module is included the whole package gets added to the bundle, even if only a part of it is used. It can be difficult for bundlers to know if code has side effects, which means that the code has dependencies on variables within scope but not within the explicit export. Without being able to distinguish what is being used and what is dead code, bundlers include the entirety of modules. This causes unnecessary additions to file size. Put another way, importing only one thing from a module with more than one export will mean the unused export is included in the bundle.
However, with some configuration, bundlers can eliminate unused exports and other dead code in a module. Eliminating dead code is often called “tree shaking”. By telling Webpack what parts of the code have no side effects it can shake the dead code out of the “tree”, or bundle. Using ESM is the most effective way to enable tree-shaking. If that isn’t an available option, which is still common today, then additional configuration is needed.
Dynamic require and lazy-loaded modules
Tree-shaking and code splitting a web application reduces the bundle file size significantly, but it doesn’t stop there. One of the main attractions of modern ECMAScript Modules is the ability to load modules as-needed. A rarely used part of an application can weigh down a bundle size unnecessarily for most users. Instead, it’s possible to dynamically load a module later instead of including it in the bundle file.
On-demand, “lazy-load” of a module is part of the specification for ES6. The syntax follows different rules, since it relies on Promises: the module is conditionally loaded when the import function is called. This means that the rarely used feature mentioned earlier is only referenced by the bundle and only loaded by the browser when a user accesses the feature, hence the on-demand nature of dynamic imports.
Adopting ESM Javascript
When I gave up the “old way” in favor of using a JavaScript bundler, it forever changed my working life for the better. Modular JavaScript is so much easier to think about and work with, and the optimizations that can be done with a compiler save so much time and headache when developing for the web. For new web developers that haven’t known the old way or for anyone still avoiding JavaScript modules, I hope this article explains how we got here and removes some of the trepidation in using ESM today.
Further Reading
- Webpack – A Detailed Introduction
- Brief History of Modularity
- CommonJS – Learning JavaScript Design Patterns by Addy Osmani
- JavaScript modules – JavaScript | MDN
- Exploring ES6, Chapter 16: Modules
- Evolution of the web
- A brief history of Node.js
Compilers
Loved the article? Hated it? Didn’t even read it?
We’d love to hear from you.