Oh CommonJS! Why are you mESMing with me?! Reasons to ditch CommonJS
Tags: javascript, typescript, esm, commonjs
Published: 2024-07-12 08:21:24

It was a normal patching day. I patched and upgraded my npm dependencies without making code changes, and suddenly, some of my unit tests failed.
![test-fail.gif](https://jolo-dev-blog-images.s3.amazonaws.com/test-fail.gif)

Wtf!

![Huh](https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExdndqOGhxM21wZGhpam14ejR3YTZiMTA2NzltNmdpMXFmdGJ4eXZwciZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/l3q2K5jinAlChoCLS/200w.webp)

My tests failed because `Jest encountered an unexpected token`; they failed because Jest cannot handle ESM-only packages out of the box. In fact, Jest is written in CommonJS.
But what does that mean? To do so, we need to understand why CommonJS and ESM exist.

## Why Do We Need Module Systems?

In the early days of web development, JavaScript was mainly used to manipulate the Document Object Model (DOM) with libraries like jQuery. However, the introduction of Node.js also led to JavaScript being used for server-side programming. This shift increased the complexity and size of JavaScript codebases. As a result, there arose a need for a structured method to organize and manage JavaScript code. Module systems were introduced to meet this need, enabling developers to divide their code into manageable, reusable units[^1].

### The Emergence of CommonJS

CommonJS was established in 2009, originally named ServerJS[^2]. It was designed for server-side JavaScript, providing conventions for defining modules. Node.js adopted CommonJS as its default module system, making it prevalent among backend JavaScript developers. CommonJS uses `require` to import and `module.exports` to export modules. All operations in CommonJS are synchronous, meaning each module is loaded individually.

### The Rise of ESM (ECMAScript Modules)

In 2015, ECMAScript introduced a new module system called ECMAScript Modules (ESM), primarily targeting client-side development. ESM uses `import` and `export` statements, and its operations are asynchronous, allowing modules to be loaded in parallel[^3]. Initially, ESM was intended for browsers, whereas CommonJS was designed for servers. It became more and more a standard for the JS ecosystem. Nowadays, modern JavaScript runtimes support both module systems. Browsers began supporting ESM natively in 2017. Even Typescript adapted the ESM syntax, and whenever you learn it, you also learn ESM subconsciously.

![How Are you not dead.jpg](https://jolo-dev-blog-images.s3.amazonaws.com/How Are you not dead.jpg)

## CommonJS is here to stay

The truth is that there are many more CommonJS (CJS)- only packages than ESM-only packages[^4].
![cjs-vs-esm.jpeg](https://jolo-dev-blog-images.s3.amazonaws.com/cjs-vs-esm.jpeg)
However, there is a clear trend. The number of ESM-only or dual module packages is on the rise, while fewer CJS-only packages are being created. This trend underscores the growing preference for ESM and raises the question of how many of the CJS-only packages are actively maintained.

### Comparison

An interesting comparison between CommonJS and ESM involves performance benchmarks. Due to its synchronous nature, CommonJS is faster when directly using require and import statements. Let's consider the following example:

```javascript
// CommonJS -> s3-get-files.cjs
const s3 = require('@aws-sdk/client-s3');
new s3.S3Client({ region: 'eu-central-1' });

// ESM -> s3-get-files.mjs
import { S3Client } from '@aws-sdk/client-s3';

new S3Client({ region: 'eu-central-1' });
```
I used the [aws-sdk S3-Client](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3/) because it has dual module support. Here we instantiate the client and then execute it with `node`:

```sh
hyperfine --warmup 10 --style color 'node s3-get-files.cjs' 'node s3-get-files.mjs'

Benchmark 1: node s3-get-files.cjs
Time (mean ± σ): 82.6 ms ± 3.7 ms [User: 78.5 ms, System: 16.7 ms]
Range (min … max): 78.0 ms … 93.6 ms 37 runs
Benchmark 2: node s3-get-files.mjs
Time (mean ± σ): 93.9 ms ± 4.0 ms [User: 98.3 ms, System: 18.1 ms]
Range (min … max): 88.1 ms … 104.8 ms 32 runs

Summary
node s3-get-files.cjs ran
  1.14 ± 0.07 times faster than node s3-get-files.mjs
```

As you can see, the `s3-get-files.cjs` and, thus, CommonJS run faster.
I got inspired by [Buns Blogpost](https://bun.sh/blog/commonjs-is-not-going-away).

However, when you want to productionize your JS library, you need to bundle it. Otherwise, you will ship all the `node_modules`. Is used [`esbuild`](https://esbuild.github.io/) because it is able to bundle to CJS and ESM.  Now, let's run the same benchmark with the bundled version.
```sh
hyperfine --warmup 10 --style color 'node s3-bundle.cjs' 'node s3-bundle.mjs'

Benchmark 1: node s3-bundle.cjs
Time (mean ± σ): 62.1 ms ± 2.5 ms [User: 53.8 ms, System: 6.7 ms]
Range (min … max): 59.5 ms … 74.5 ms 45 runs

Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet system without any interferences from other programs. It might help to use the '--warmup' or '--prepare' options.

Benchmark 2: node s3-bundle.mjs
Time (mean ± σ): 45.3 ms ± 2.2 ms [User: 38.1 ms, System: 5.6 ms]
Range (min … max): 43.0 ms … 59.2 ms 62 runs

Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet system without any interferences from other programs. It might help to use the '--warmup' or '--prepare' options.

Summary

  node s3-bundle.mjs ran
    1.37 ± 0.09 times faster than node s3-bundle.cjs
```
As you can see, the `s3-bundle.mjs` is now faster than the `s3-bundle.cjs`. The ESM file is now even faster than the unbundled CommonJS file because it results in smaller file sizes and faster load times due to efficient tree-shaking—a process that removes unused code.

## Embrace ESM!

The future of JavaScript modules is undoubtedly leaning towards ESM. This starts when creating a new NodeJS project or even a React project. Every tutorial and article uses the `import`—statement, which is thus ESM. Despite many existing CommonJS packages, the trend is shifting as more developers and maintainers adopt ESM for its performance benefits and modern syntax. Another question is also how many of these CJS-only projects are still maintained.

ESM is a standard that works in any runtime, such as NodeJS, Bun, or Deno, and in the browser without running on a server. It is unnecessary to convert via Babel to CommonJS because the browser understands ESM. You can still use Babel to convert to a different ECMAScript version, but you shouldn't convert to CJS.

You should develop in ESM-only because every runtime now and browser newer than 2017 understands ESM.

If your code breaks, you may have legacy issues. Consider using different tooling or packages. For example, you can migrate from Jest to [`vitest`](https://vitest.dev/) or from ExpressJS to [`h3`](https://h3.unjs.io/). The syntax remains the same; the only difference is the import statement.

**Key Takeaways**:

- **Smaller Bundles**: ESM produces smaller bundles through tree-shaking, leading to faster load times.
- **Universal Support**: ESM is supported natively by browsers and JavaScript runtimes (Node.js, Bun, Deno).
- **Future-Proof**: With ongoing adoption, ESM is positioned as the standard for modern JavaScript modules.

To get started, you can follow this [Gist](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c) or get an inspirational learning [here](https://blog.isquaredsoftware.com/2023/08/esm-modernization-lessons/).

For a better JavaScript Future, Embrace ESM!

[The presentation](https://youtu.be/hp4g23Zsmaw)

## More Resources

- [https://dev.to/logto/migrate-a-60k-loc-typescript-nodejs-repo-to-esm-and-testing-become-4x-faster-22-4a4k](https://dev.to/logto/migrate-a-60k-loc-typescript-nodejs-repo-to-esm-and-testing-become-4x-faster-22-4a4k)
- [https://jakearchibald.com/2017/es-modules-in-browsers/](https://jakearchibald.com/2017/es-modules-in-browsers/)
- [https://gist.github.com/joepie91/bca2fda868c1e8b2c2caf76af7dfcad3](https://gist.github.com/joepie91/bca2fda868c1e8b2c2caf76af7dfcad3)
- [https://gist.github.com/joepie91/bca2fda868c1e8b2c2caf76af7dfcad3](https://gist.github.com/joepie91/bca2fda868c1e8b2c2caf76af7dfcad3)

[^1]: https://www.freecodecamp.org/news/javascript-es-modules-and-module-bundlers/#why-use-modules
[^2]: https://deno.com/blog/commonjs-is-hurting-javascript
[^3]: https://tc39.es/ecma262/#sec-overview
[^4]: https://twitter.com/wooorm/status/1759918205928194443