JavaScript module formats

Updated on Dec 01, 2020/javascriptjs-modules

JavaScript programs started off pretty small, the official specification only defined APIs for some objects that were useful for building browser-based applications.

Most of its usage in the early days was to do isolated scripting tasks, providing a bit of interactivity to your web pages where needed, so large scripts were generally not needed.

Over the years, there have been multiple attempts both official and unofficial to bring JavaScript to other environments like web servers, command-line tools, desktop applications, and hybrid applications.

In modular programming, developers break programs up into discrete chunks of functionality called a module. Each module has a smaller surface area than a full program, making verification, debugging, and testing trivial. Well-written modules provide solid abstractions and encapsulation boundaries, so that each module has a coherent design and a clear purpose within the overall application.

JavaScript has had modules for a long time. However, these formats were implemented via libraries, not built into the language:

ES6 is the first time that JavaScript has built-in modules:

CJS

CJS is an ambitious project started by Kevin Dangoor back in 2009 in an attempt to bring JavaScript to the outside of web browsers, originally called ServerJS. The ultimate goal is to define a JavaScript Standard Library like other general-purpose programming languages and compatible in multiple host environments.

CJS APIs also define a module format in which you can export an object in some modules and require synchronously in other modules.

CJS modules basically contain two primary parts: a free variable named exports, which contains the objects a module wishes to make available to other modules, and a require function that modules can use to import the exports of other modules

// utils.js
const log = (message) => {
  console.log(message)
}
module.exports = {log}
// index.js
const utils = require('./utils')
utils.log('Hello World')

The CJS modules work fine in a local environment, but they did not fully embrace some things in the browser environment that cannot change but still affect module design: network loading and inherent asynchronicity.

AMD

The initial attempt was a CJS transport format, then changed over time to become a module definition API.

AMD is a module format that allows module and its dependencies can be asynchronously loaded. This is particularly well suited for the browser environment where synchronous loading of modules incurs performance, usability, debugging, and cross-domain access problems.

The specification uses define function to define named or unnamed modules based on the following signature:

define(
  module_id /*optional*/,
  [dependencies] /*optional*/,
  definition function /*function for instantiating the module or object*/
);

Example of defining a named module

//Calling define with a dependency array and a factory function
define('awesome-module', ['dep1', 'dep2'], function (dep1, dep2) {
  //Define the module value by returning a value.
  return function () {}
})

UMD

Since CJS and AMD styles have both been equally popular, it seems there’s yet no consensus. This has brought about the push for a universal pattern that supports both styles, which brings us to none other than the UMD.

The pattern is ugly, but is both AMD and CJS compatible, as well as supporting the old-style global variable definition.

// Uses AMD or browser globals to create a module.
;(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD. Register as an anonymous module.
    define(['b'], factory)
  } else {
    // Browser globals
    root.amdWeb = factory(root.b)
  }
})(typeof self !== 'undefined' ? self : this, function (b) {
  // Use b in some fashion.

  // Just return a value to define the module export.
  // This example returns an object, but the module
  // can return a function as the exported value.
  return {}
})

ESM

ESM is the ECMAScript standard for working with modules, this standardization process completed with ES6 and browsers started implementing this standard trying to keep everything well aligned.

The goal for ESM was to create a format that both users of CJS and of AMD are happy with: Similarly to CJS, they have a compact syntax, a preference for single exports and support for cyclic dependencies. Similarly to AMD, they have direct support for asynchronous loading and configurable module loading.

Within a module, you can use the export keyword to export just about anything, you can export a const, a function, or any other variable binding or declaration.

// hello.js
export const apiKey = 'random-key-her'
export const doSomething = () => {
  console.log('hello there')
}
export default doSomething

You can then use the import keyword to import the module from another module.

// main.js
import doSomething, {apiKey} from './hello'

console.log(apiKey)
doSomething()

Above ESM syntax using import and export called static imports which allow static analysis that helps with optimizations like tree shaking and scope-hoisting, and provide advanced features like circular references and live bindings.

The newest part of the ESM functionality to be available in browsers is dynamic imports. This allows you to dynamically load modules only when they are needed, rather than having to load everything up front. This has some obvious performance advantages.

import('./awesome-module.js').then((module) => {
  // Do something with the module.
})

Summary

Even though JavaScript never had built-in modules, the community has converged on a simple style of modules, which is supported by libraries in ES5 and earlier. Then ES6 came out as a major update with native module support.

  • CJS - has various implementations, including Node.js, it was not particularly designed with browsers in mind, so it doesn’t fit in the browser environment very well.
  • AMD - more suited for the browser because it supports asynchronous loading of module dependencies.
  • UMD - admitted ugly but is both AMD and CJS compatible.
  • ESM - the ECMAScript standard for working with modules since ES6.

Modern browsers have started to support module functionality natively, they can optimize loading of modules, making it more efficient than having to use a library and do all of that extra client-side processing and extra round trips.