The JavaScript ecosystem is quickly moving toward publishing ECMAScript modules (ESM) instead of CommonJS modules. Is it ready for production?

The JavaScript ecosystem is quickly moving toward publishing ECMAScript (ES) modules (ESM) instead of CommonJS modules. To keep up with new JS changes, I often learn and play with the new ones in my side projects. But the real question is are they ready for production yet?

About my last big production ready application Link to heading

In my former company, I developed an application using NodeJS + GraphQL as backend for frontent (BFF) and use React for rendering UI. They are all written in Typescript. We built it from beginning but on top of a template which is defined awhile back by the org. It started as smooth as we hoped it to but the more we developed, the worse DX became.

I have latest version of Node, can I use this ESNext feature? NO!! YOU DON’T
This is my biggest regret when I didn’t look into the template configuration at all, assuming they are configured by the pros, and they know better than I do. That assumption is true, but I didn’t expect that they built that template a few years ago. But Javascript has a “jet engine” (JIT), that can travel at speed of light 😱 When you use node 16 and write array.flatMap, string.replaceAll, TS will yell at you, that thing does not exists in current targeted version. Even though that syntax is perfectly valid at runtime, and you’re sure about that, you can’t touch it…

You might argue that I can still change the tsconfig target to add new ES features to my project. Of course I thought that too. But it was easier thought than done. You can’t just edit a json file and walk away. Newer targets require higher major TS version. And I had nightmare during that time because I couldn’t handle the errors pop up all over the places. Then the product needs more features, I shouldn’t holding on it for too long. So I decided to declare a compatible type to be able to use it and turn away.
Fun fact: while I’m writing this post and opening node@18.x.x version not fetch global api issue in a browser tab. There are 2 more comments and 1 pull request added to it 🤣

Of course this could be avoided in the beginning if we configured typescript to match the node version we intend to use. But we would introduce a different variant from organization standard, and in my former company, we love standard, stability, predictability (that’s why they built a project template with TS in the first place). Though, upgrading the template to newer TS version is an other nightmare.

Of course TS is very useful when you don’t know what your teammate wrote in the other component last week, and you don’t want to re-read their code to use it probably.

But without types, code editor can’t do static analysis and autocomplete Link to heading

This point is valid. Without type definitions, code editor can’t be smart enough to analyze our code. Function arguments can be of any types, so it can’t suggest you what methods you can call on an object. But that problem has been solved long before TS is a thing. We had JSDocs for the rescue the whole time. If you don’t mind writing type definitions with TS, I believe it feel the same writing JSDocs typedefs. And it doesn’t require any other tools added to you project. And JSDocs is JS in mind so it doesn’t introduce new concepts that does not belong to Javascript like TS does. Everything you write can simply run anywhere.

Hello Webpack my old friend Link to heading

CommonJS was a solution, but not an official ES feature for modularization JS code. Node and other server-side runtimes adopted it and quickly became a standard to write JS back then. Then modules spec was proposed to ES with import, export keywords. Browsers and server runtime now can use the same syntax to import modules, can’t they? The answer is NO! Not until it pass stages of reviews and is added to the spec. But community like it. So we start using it without permission. With the help of Babel and Webpack, developers can write code in the style they like. And ship it in a different format that no one ever look at. They are used in almost every projects. Hence, when I start a new project, I google some Balbel + Webpack configuration, put it in my project, write some code, try adding some configurations if my code need, forget about the configuration, and repeat all of these steps again for next projects. Repeating dummy steps without any takeaway make me also resist using Babel+Webpack too (sure they are still useful for frontend application for performance reasons, and libraries that need to support different runtimes). If they are needed for UI app, let the library, framework handle it, create-react-app, vue create, sveltekit… Those who introduce new weird syntaxes have to carry the burden they made not us. Or because we chose them instead of writing vanilla JS, it’s on us too, lol?
I use libraries, I don’t write libraries, so not until then
But for server side, let’s answer a few questions.

  • Does it help improving application startup time? Maybe, maybe not (JIT compiler is the one decides how your code is parsed and executed at runtime, not TS or Babel).
  • Does it allow you to write new syntax and run on any server runtime? Why do you random node version on server you control? Just pick latest LTS version.
  • Does it improve performance? I don’t thing so, or even worse, they replace a native API with a polyfill which is less performance.
  • If you have an API bug, are you sure the code you wrote and run on local is exactly the same as your server is running?
  • Does it reduce bundle size?
    If your final distribution include your code and built code, it is doubled in size.
    If you build your code and only distribute built code, you double the build time.
    If you don’t use Babel and Webpack, you don’t double anything.

If ESM is so good, why don’t we start using it for production application?
There are a few downsides of ESM at the moment. We often include bunch of libraries in our project, which includes more dependencies. Those libraries might not distributed a ESM compatible variant.
Some logging, tracing tools are heavily implemented on top of CommonJS features like opentelemetry. We developer can wait to add it to our code, but production application can’t wait to spill out bugs and by that time you start to regret not having a tracing agent. Testing frameworks too.

If you don’t use those tools, why not try native ESM? Just remove TS, Babel, Webpack dependencies, add "type": "module" to your package.json. And the most hard working part, adding .js extension to all your relative imports. Make sure you use Node version 16 or higher. You can start rolling with ESM.