Understanding JavaScript Modules: Bundling & Transpiling

Share this article

This article was peer reviewed by Dan Prince and Ravi Kiran. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

Most folks consider modules, dependency management and dynamic loading a basic requirement for any modern programming language—these are some of the most important features added to JavaScript in 2015.

Modules are used extensively in Node but our focus here will be on how we can use modules inside of the browser. We’ll explore a little history, navigate through the hazardous current landscape with the end goal of having a clear path forward and appreciation for the most important module bundlers for JavaScript today: Browserify, Webpack and jspm.

Finally we’ll look at how to use these tools with transpilers like CoffeeScript, TypeScript and Babel.

Modules through the Ages

JavaScript has existed since 1995 and to this day no browser supports modules natively. Node and CommonJS were created in 2009 and the vast majority of packages in npm use CommonJS modules.

Browserify was released in 2011 and brought CommonJS modules to the browser allowing client-side JavaScript to require npm packages. The tool bundles up all of the required dependencies into a single JavaScript file.

The Past

A library such as jQuery adds $ to the global scope or window.

window.$ = function() { ... };

We include a script to a library and use the global objects it exposes.

<script src="jquery.js"></script>
<script>
$(function() { ... });
</script>

Your own application code was typically namespaced under a global like App to prevent polluting the global scope. Without this it’s only so long before you have name collisions and things fall apart.

var App = {};
App.Models = {};
App.Models.Note = function() {};

The Future

Libraries export objects in a common module format (ES6 modules).

export default function $() { ... }

We import a module into a local scope and use it.

import $ from 'jquery';

$(function() { ... });
  • No globals required 👍
  • Source order independence
  • Access to npm
  • No need to namespace your own application code
  • Dynamically load modules at any time as required

The Present

It’s really really complicated. First, there’s a variety of module formats out there in use:

Tools for bundling assets come in a variety of shapes and sizes:

Then there’s transpilers that you may want to use:

In addition, there are various libraries that allow dynamic loading of modules:

These are shortened lists of popular tools currently in use—it’s a minefield for beginners and experts alike. The cost of transpiling also highlights that you can mix and match a lot of these tools and get different results.

Let’s Consolidate Tooling in 2016

Front-end developers have been using build tools for a very long time but it’s only in the last few years that we’ve seen a build step become the norm. Tools like Sass and CoffeeScript helped make pre-processing mainstream but the momentum around ES6 has now got everyone on board.

I agree.

Gulp and Grunt have been very popular in the past few years, these tools allow you to write a series of transforms to pipe your assets through. They’ve been used to great effect and are still popular, though a lot of people choose to use the tools directly through npm – see Why I left Gulp and Grunt for npm Scripts and Guide to using npm as a Build Tool.

Personally, I don’t care for building asset pipelines any longer, what I’m looking for is minimal config tools that allow me to use modern tooling as needed: Things like Sass, Autoprefixer, Babel and Coffeescript, a proper module system and loader without having to worry about the implementation, configuration and ongoing maintenance. In essence, every developer has been investing time into creating asset pipelines over the last few years, that’s a lot of wheel re-invention going on and a lot of wasted hours.

The community is divided across tools like Browserify, Webpack, jspm, Sprockets and Gulp. That’s not really a problem, it’s just confusing for everyone trying to understand a clear path forward.

Clear Starting Points

There’s a few things we can agree on:

  • ES2015 modules are the one true future module format for JavaScript.
  • Babel is the ES2015 compiler of choice today.
  • Native loaders are still a while away from being available in browsers, a report on the Future of JavaScript by Telerik suggests that complete ES2015 support could take over two years given the module loading hurdle.
  • If you want to use modules now, that will very likely involve CommonJS at some point.

Let’s look at what minimal configuration setups look like using Browserify, Webpack and jspm, these are the most important JavaScript bundlers to know about today.

A New Project

mkdir modules-app
cd modules-app
npm init -y
npm install --save-dev browserify webpack jspm
mkdir src
touch src/{entry,lib}.js index.html

Update index.html in your favourite text editor

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Modules!</title>
</head>
<body>
  <script src="bundle.js"></script>
</body>
</html>

We’ll also need a server to run the code—for example live-server which is a great little zero-config HTTP server with live reload capability. Install it globally with npm install -g live-server and run live-server from the project root to start.

Browserify

Browserify lets you require('modules') in the browser by bundling up all of your dependencies.

Open up src/lib.js and add our very first module.

var double = function(number) {
  return number * 2;
}

module.exports = {
  double: double
}

Open up src/entry.js and we’ll require our module and use it.

var lib = require('./lib.js');
console.log(lib.double(2));

Update the scripts section in package.json

"scripts": {
  "browserify": "browserify ./src/entry.js -o ./bundle.js"
},

Run this script with npm run browserify

Browserify will create bundle.js in the project root and you should see a most exiting 4 output to the console. To learn more about what Browserify is doing and how this bundle gets created I recommend watching Introduction to Browserify at egghead.io

Congratulations! We now have modules in the browser! 🎉

Another key benefit of Browserify is that it gives you access not only to modules that you author, but to npm modules as well, let’s install lodash to see.

npm install lodash --save-dev

Edit src/lib.js

var sum = require('lodash/sum');

var double = function(number) {
  return number * 2;
}
var addFive = function(number) {
  return sum([number, 5]);
}

module.exports = {
  double: double,
  addFive: addFive
}

Edit src/entry.js and call our new addFive function

var lib = require('./lib.js');
console.log(lib.double(2));
console.log(lib.addFive(2));

Create the bundle again with npm run browserify and in the browser you should see a 4 and a 7 which shows that we’ve successfully imported and used lodash’s sum function.

If you’ve followed along this far you now know everything you need to get started using modules in the browser today, this brings many benefits that we outlined at the start.

  • No globals required 👍
  • Source order independence
  • Access to npm
  • No need for namespacing your own application code

We’ll look at dynamic loading of modules at runtime later.

Webpack

Webpack is a module bundler. Webpack takes modules with dependencies and generates static assets representing those modules.

Let’s add a new script to package.json for calling webpack

"webpack": "webpack ./src/entry.js bundle.js"

Run it with npm run webpack

Webpack will have rewritten bundle.js and the output in the browser should be exactly the same.

Try running npm run browserify and npm run webpack and examining the differences in the compiled bundle.js file. It’s not really important to understand how these tools work internally, the important thing to note is that whilst the implementations are different they are essentially doing the same task of compiling the same code with CommonJS modules into standard browser-friendly JavaScript. Each module is put inside a function within bundle.js and assigned an ID so that it can be loaded as required.

There’s far more to Webpack than this though! It truly is the swiss army knife of module bundlers. Webpack also comes with great tools for development out of the box, things like hot module replacement which will automatically reload individual modules in the browser as they are changed—similar to LiveReload but without the page refresh.

There is a growing list of loaders for different asset types too, even CSS with the css-loader and style-loader—loaders which can compile CSS into the JavaScript bundle and inject it into the page at runtime. This is outside of the scope of this article but more can be found on this at getting started with Webpack.

JavaScript Transpilers

These are three of the most popular transpilers used today, you may also want to use another from the very long list of languages that compile to JS.

Before looking at how we can use them with our module bundlers let’s look at how to use the tools directly first.

npm install --save-dev coffee-script typescript babel-cli babel-preset-es2015
touch src/{coffee-lib.coffee,ts-lib.ts,es6-lib.js}

CoffeeScript

Edit coffee-lib.coffee

sum = require 'lodash/sum'

double = (number)-> number * 2
addFive = (number)-> sum([number, 5])

module.exports =
  double: double
  addFive: addFive

Note: CoffeeScript uses the CommonJS syntax for modules

Add a script to package.json to run the coffee executable

"coffee": "coffee --output ./dist ./src/coffee-lib.coffee"

Run it with npm run coffee

TypeScript

Edit ts-lib.ts

/// <reference path="lodash.d.ts" />
import * as _ from 'lodash';

const double = (value: number)=> value * 2
const addFive = (value: number)=> _.sum([value, 5])

export = {
  double,
  addFive
}

Note: TypeScript has its own syntax for modules that look like a mix of ES2015 module syntax and CommonJS.

Add a script to package.json to run the tsc executable

"tsc": "tsc --outDir ./dist ./src/ts-lib.ts"

Run it with npm run tsc

The compiler will complain about not being able to find lodash as it requires a type definition to know how to work with external modules that aren’t TypeScript files. You can fetch a definition file with:

cd src
curl -O https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/master/lodash/lodash.d.ts
cd ..
npm run tsc

Babel

Edit es6-lib.js

import sum from 'lodash/sum';

const double = (number)=> number * 2
const addFive = (number)=> sum([number, 5])

export {
  double,
  addFive
}

Note: Babel understands the lovely new ES2015 module syntax.

Babel requires a config file for specifying which presets to use

echo '{ "presets": ["es2015"] }' > .babelrc

Add a script to package.json to run the babel cli

"babel": "babel ./src/es6-lib.js -o ./dist/es6-lib.js"

Run it with npm run babel

The files in /dist now contain ES5 code in CommonJS module format that will work perfectly with Browserify or Webpack as we used previously. You can either transpile down to ES5 with CommonJS first and then bundle, or you can use other packages to do both in a single step.

For Browserify there are plugins coffeeify, tsify and babelify to transpile and bundle.

For Webpack there are loaders coffee-loader, ts-loader and babel-loader to require modules in different languages.

jspm

jspm is a package manager for the SystemJS universal module loader, built on top of the dynamic ES6 module loader

jspm takes a different approach and starts with the module loader System.js. System.js is a project that will follow the loader spec as it’s developed.

Install and initialize a jspm project

npm install -g jspm
jspm init

Accept all of the defaults and ensure that Babel is used as the transpiler, that will configure System.js to use Babel when it runs into ES6 style modules.

Update index.html to load and configure System.js

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Modules!</title>
</head>
<body>
  <script src="jspm_packages/system.js"></script>
  <script src="config.js"></script>
  <!--<script src="bundle.js"></script>-->
  <script>
   System.import('src/entry.js');
  </script>
</body>
</html>

In the browser you’ll see a few requests being made and a 404 for lodash, this is because jspm loads packages from the jspm_packages directory by default.

Run jspm install lodash to install lodash in that directory and you should see the expected output in the console, a 4 and a 7, here’s what’s happening:

  • Our entry.js file is being dynamically loaded with System.import('src/entry.js');.
  • System.js loads entry.js, sees that it requires our lib module so fetches it at runtime.
  • System.js loads lib.js, sees that it requires lodash/sum and fetches it too.

System.js also knows how to work directly with ES6, update entry.js to dynamically require our ES6 module and compile on the fly.

import lib from './es6-lib';
// import lib from '../dist/coffee-lib';
// import lib from '../dist/ts-lib';

console.log(lib.double(2));
console.log(lib.addFive(2));

You can also try loading the ES5 compiled versions of our CoffeeScript or TypeScript modules by uncommenting those lines one at a time. Another option is to use System.js plugins to transpile the code, rather than requiring precompiled ES5 code.

Add a final script to package.json for creating a bundle with jspm

"jspm": "jspm bundle src/entry bundle.js"

Run it with npm run jspm

Finally uncomment the script tag for bundle.js in index.html and the browser should load a production-ready bundle without any extra http requests.

<script src="bundle.js"></script>

Revisiting Webpack

Our Webpack example earlier was the simplest example using the default options, it compiled entry.js with CommonJS modules down into a single bundle. When doing more fancy things with Webpack you’ll want to create a custom config file for all of the loader configuration.

Create webpack.config.js in the root of the project

module.exports = {
  context: __dirname + "/src",
  entry: "./entry",
  output: {
    path: __dirname,
    filename: "bundle.js"
  },
  module: {
    loaders: [{
      test: /\.js$/,
      loader: 'babel-loader',
      query: {
        presets: ['es2015']
      }
    },{
      test: /\.coffee$/,
      loader: 'coffee-loader'
    },{
      test: /\.ts$/,
      loader: 'ts-loader'
    }]
  }
}

Update index.html to load only the bundled file again.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Modules!</title>
</head>
<body>
  <script src="bundle.js"></script>
</body>
</html>

Install the loaders for transpiling with Babel, CoffeeScript and TypeScript

npm install --save-dev babel-loader coffee-loader ts-loader

Install webpack globally and run without arguments to create the bundle from our config file.

npm install -g webpack
webpack

Now that webpack knows to use these loaders for these file extensions we’re free to use ES6, CoffeeScript or TypeScript from entry.js, give it a try by uncommenting these one by one.

import lib from './es6-lib.js';
// import lib from './coffee-lib.coffee';
// import lib from './ts-lib.ts';

There is so much more to Webpack than I’ve covered here, but these simple setups are a great starting point.

There and Back Again

And so we end our exploration of modules, they do solve a lot of problems and can really reduce the complexity of our applications—if the tooling doesn’t get in our way. If you’re not already using modules, now is the time. No need to spend unnecessary hours building asset pipelines, instead use simple tools that Just Work™.

Webpack is the current juggernaut and you’ll be able to configure it to do almost anything. jspm is a great tool for all your bundling needs and works with a variety of formats and has a nice developer experience. Browserify is still a solid option, the grandfather of modern module bundlers—it’s ecosystem has grown to include some of Webpack’s much loved features (such as bundle splitting and hot reloading). Finally System.js is perfect for when you need to be able to load extra modules at runtime.

You won’t want to use all of the above tools in one project but it’s important to have an understanding of these three popular options, as well as how you can use transpilers when the need arises. If you just want to use modules, then Browserify, jspm or Webpack with the default options will do the job.

Keep the tooling simple and the configuration light. Happy Bundling.

Frequently Asked Questions (FAQs) about JavaScript Modules, Bundling, and Transpiling

What is the difference between JavaScript modules, bundling, and transpiling?

JavaScript modules are individual units of code that can be imported and used in other scripts. They help in organizing code, making it more maintainable and reusable. Bundling, on the other hand, is the process of combining multiple JavaScript files into a single file. This is done to reduce the number of HTTP requests, which can significantly improve the performance of a web application. Transpiling is the process of converting source code from one language or version to another. In the context of JavaScript, it often refers to converting newer JavaScript syntax into older versions for compatibility with older browsers.

Why is bundling important in JavaScript?

Bundling is important in JavaScript for several reasons. Firstly, it reduces the number of HTTP requests by combining multiple JavaScript files into one. This can significantly improve the performance of a web application. Secondly, bundling allows you to use modules in browsers that do not support them. Lastly, bundling can also include a process called minification, which reduces the size of the JavaScript file, further improving performance.

What is the role of a JavaScript transpiler?

A JavaScript transpiler, like Babel, plays a crucial role in ensuring that JavaScript code is compatible across different browsers. It does this by converting newer JavaScript syntax into older versions that are supported by most browsers. This allows developers to use the latest features of JavaScript without worrying about browser compatibility issues.

How does a JavaScript module work?

A JavaScript module is a standalone piece of code that exports specific variables or functions that other modules can use. When a module is imported, its exported variables or functions become available to the importing script. This allows for code reuse and better organization of code.

What are some popular JavaScript bundling tools?

Some popular JavaScript bundling tools include Webpack, Rollup, and Parcel. These tools not only bundle JavaScript files but also offer features like code splitting, lazy loading, and handling of other assets like CSS and images.

How does transpiling help in writing modern JavaScript?

Transpiling allows developers to write modern JavaScript using the latest features and syntax, without worrying about compatibility issues. The transpiler converts the modern JavaScript into a version that is compatible with most browsers. This ensures that the JavaScript code runs consistently across different browsers.

Can I use JavaScript modules without bundling?

Yes, it is possible to use JavaScript modules without bundling. However, this might lead to performance issues due to multiple HTTP requests. Also, not all browsers support JavaScript modules, so bundling can help ensure compatibility.

What is the difference between bundling and minification?

Bundling is the process of combining multiple JavaScript files into one to reduce the number of HTTP requests. Minification, on the other hand, is the process of removing unnecessary characters (like spaces and comments) from the JavaScript file to reduce its size. Both bundling and minification can improve the performance of a web application.

How do I choose the right bundling tool for my project?

The choice of a bundling tool depends on the requirements of your project. Factors to consider include the size of your project, the features you need (like code splitting or handling of other assets), and your personal preference. It’s a good idea to try out a few different tools and see which one works best for your project.

What is the future of JavaScript modules, bundling, and transpiling?

The future of JavaScript modules, bundling, and transpiling looks promising. With the continuous evolution of JavaScript, we can expect more advanced features and syntax, which will require transpiling for compatibility with older browsers. Bundling will continue to be important for performance reasons. As for modules, they are becoming a standard part of JavaScript, and we can expect better support for them in browsers.

Mark BrownMark Brown
View Author

Hello. I'm a front end web developer from Melbourne, Australia. I enjoy working on the web, appreciate good design and working along side talented people I can learn from. I have a particular interest in visual programming so have fun working with SVG and Canvas.

AMDBabelBrowserifyCommonJSES2015es6jameshjavascript modulesjspmlearn-modernjstranspilersUMDwebpack
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week