Native JavaScript Modules

A decade (or more) in the making, JavaScript modules are now supported in all major browsers and in Node.js. So now there’s a widely supported, standardized way of modularizing your JavaScrpt.

In this presentation Mejin Leechor will show us how they work, and why you should be using them.


Native JavaScript Modules

Mejin Leechor, Software Engineer VMWare

Mejin started learning JS about three years ago and the import and export statements caught her eye – they made her realise her impression of JS was very outdated. She hadn’t used it since she was nine, making a Geocities site and using JS to add sparkly cursor trails.

The web in 2020 is very different from those days, but despite all the progress support for native modules is surprisingly recent. To clarify ‘native modules’:

✖ Not CommonJS:
const express = require('express')

✖ Not AMD

define(['jquery'], function ($) {
// Module body
return someModuleExport;
})

EcmaScript Modules, aka ES Modules, are the only import and export mechanism to make it into the native language. You may be using them in a library like React:

import x from "module";
import * as namespace from "module";
import {x} from "module";
// ...and more

This specification may be newer than you expect – they were added to the ECMAscript spec in 2015, but only got support in browsers in 2018 and nodejs in 2019. Getting native modules took years of community effort.

So what’s all the fuss about modules? Why did we need them?

First they provide modularity – structure through boundaries. You can also think of this as building blocks for your code, small pieces that can be composed into bigger things.

They not only expose their own interface, they can express dependencies on other modules. Modules also get private scopes, beyond what you can do with namespaces.

The benefits of modularity include being able to reason about one piece of your code at a time; which encourages looser coupling and more-maintainable code. It also enables code re-use.

Modularity is for humans. We suffer when our code is difficult to manage, and benefit when it’s easier to understand and maintain.

So let’s consider modules vs modularity. Modules are a means to gain modularity, but you can also break that with tightly-coupled modules.

So how did we get here? Let’s take a look at a rough timeline of module history, in four broad eras…

  • pre-modules: mid 90s to early 00s
  • DIY modules: early to late 00s
  • Specification: early to mid 10s
  • Standardisation: mid to late 10s

In the early days there wasn’t a clear need for modular JavaScript – it was intended to be embedded in web pages, web ‘applications’ weren’t a thing and JS wasn’t imagined as a language for codebases large enough to need modularity.

That gave way in the 2000s with the rise of AJAX driven web applications (particularly Gmail) alongside websites. Global variables became a huge problem, leading to DIY solutions like the module pattern and IIFE (Immediately Invoked Function Expression). It was an ingenious use of function scope, a trick that reappears many times.

Other problems turned up in this era; including dependency management, performance issues and lack of reusability. This led to the next phase of module history – when specifications began to emerge.

Part of this was driven by JavaScript’s evolution into a server-side language (from sites, to apps, to server…).

Kevin Dangoor’s 2009 article What Server-Side JavaScript needs captures the zeitgeist of the time, and set out a wish list:

  • module system
  • package management system
  • package repository

Dangoor recognised that these things would be popular; and also that they were community organisation problems more than technical problems. He went on to form a group called CommonJS, but that was not where the specification called CommonJS eventually came from.

This was great for server-side JS, but it wasn’t ideal for the browser; which spawned Asynchronous Module Definition (AMD) which performed better in the browser.

There was also an attempt to unify both server- and client-side with Universal Module Definition (UMD), but the syntax wasn’t great and it never really took off.

This lack of unified module definition remained a problem.

One other major development emerged in the specification era: bundling. This is when we stitch together multiple modules for deployment. eg. Browserify, Webpack, Rollup and Parcel.

So in the mid 00s we finally converged on a single definition: ES Modules. There’s a big range of import/export options here – default exports, named imports, named imports, aliased named imports…

import * as arithmetic from ‘./arithmetic.js'
import sweetExport from ‘./cool-module.js’
import { default as sweetExport } from './cool-module.js'

The things you import and export are read-only live bindings. That’s very different from CommonJS so be aware of that if you are changing from one to the other – and why it was hard to change in nodejs.

If you do want a dynamic import, there is support for that now too using a promise-based API (useful for lazy loading):

import('./baby-bird.js').then(module => module.drawBird())

There is now native support for modules in the browser:

<script type="module" src="my-module.js"></script>
<script nomodule src="for-non-supporting-browsers.js"></script>

You can use it now but you do need to specify a nomodule fallback for older browsers.

So are we there yet? Have we arrived? Not really. We have native modules, but they exist alongside all the options that went before; and there are still interoperability issues to be resolved.

You’re probably using module syntax during development and bundling the result – even though some browsers could use modules.

The reason for this is performance. For a long time the wisdom has been to ship one big bundle because it was better for performance; and a 2018 study by the Chrome team still recommended bundling even with HTTP2. We still haven’t quite found the right combination to make the shift.

To think about your options, consider the ES Module Maturity Model:

0: no modules
1: modules syntax and bundling
2: unbundled dev (only bundling for production)
3: native, optimised modules in production

You can opt in to whichever level you prefer and gives you value. If level three is not possible today, maybe we’ll get there in future. Judging by our past, it seems likely that we will!

@mejincodes