ES-Modules: For a faster JavaScript?

ES-Modules: For a faster JavaScript?

ES Modules are an official, standardized module system for JavaScript. But what exactly does that mean, which problems does it solve and how do ES Modules work?

Modular systems are useful. They provide a way to reuse code across different applications and platforms. They can be used in any other module via imports and exports. They are modular, can be edited and deleted independently of one another without the entire application crashing.

ES Modules are not the first attempt to add module functionality to JavaScript. CommonJS, a module system for Node.js, has been around for years. It was developed to fill this very gap. CommonJS enables exactly this modularity. Useful modules can thus be combined into packages and published via npm. Well-known examples of such packages are, for example, React, Lodash or jQuery.

Up to ECMAScript 6 there was no module system for browsers. With ECMAScript 6, ES Modules were added to the JS specification. The format is now supported by all major browsers – Safari, Chrome, Edge and Firefox. Node has also been supporting ES Modules for some time.

The advantage here: With ES Modules, JS modules can theoretically be indexed and cached in such a way that they can be accessed from anywhere. The usefulness is obvious: the modularization theoretically makes it possible for the browser to only have to fetch the affected files when changes occur. Why is this relevant? Up to 90 percent of the code on a website comes from open source packages (React, Lodash, jQuery), which have to be reloaded by the browser each time the source code is changed.

Anyone who programs in JavaScript juggles a lot with variables. Most of the time, it actually involves assigning values ​​to variables, adding numbers, or combining variables and storing them in another. Because that makes up such a large part of working with JavaScript, the way you organize these variables within a code base has a not inconsiderable influence on how well you find your way around them, how well you can code, and how easily or less just you can maintain your code.

It helps to only have to think about a few variables at a time. In JavaScript, this is achieved through a concept called Scope. It prevents functions from accessing variables that have been defined in other functions. In itself, that’s a good thing. When you’re working on a feature, you don’t have to think about what’s going on outside of scope. The obvious disadvantage: it is not possible to access it from outside the scope in which a variable is defined. If you want to do that, you have to define this variable in a higher scope, for example as a global variable.

This can be illustrated quite well with jQuery: In order to load jQuery plugins, developers had to ensure that jQuery was in global scope. Defining jQuery globally works, but it created other difficulties: You have to be careful that all script tags are in the correct order – and that no one messes up this order. If a function does not find jQuery where it expects to find it – in the global scope – your application will no longer be executed and you will get an error message.

This behavior makes it difficult to maintain a codebase. Deleting code or removing script tags becomes a gauntlet. You never know what you might break with such changes. This is because the dependencies between your code are implicit – not clearly formulated anywhere. After all, every function can access all global variables. That’s why you never know exactly which functions depend on what. In principle, code in the global scope can change variables that are also defined globally. It’s not always a good thing. Global variables offer points of attack for malicious code and generally more opportunities for bugs to arise.

Modules and the module scope

You can use modules to group these globally defined variables and functions into module scopes. The module scope allows variables to be used jointly among the functions that are in a common module scope. You can make the variables within a module scope – unlike those within a function – available for other modules. A module scope can be used to explicitly specify which of the variables, classes or functions it contains can be accessed from outside.

The process of making them available is called an export. Such an export enables other modules to make it explicit that they are dependent on a variable, class or function. Through this explicit dependency, you then know exactly which modules you are breaking when you change or remove variables, classes or functions. This makes it easier to split code into smaller pieces that also work independently of each other. And which can then be combined into any number of different applications.

And this is how the modules work

If you use modules when developing, a dependency graph or diagram is created. The connections between different dependencies are established via import statements. From these statements, the browser knows exactly which code needs to be loaded. You basically give the browser a file that it can use to access the dependency graph. From there he can find further code via further import statements.

The syntax for importing a module looks like this:

import module from 'module-name'

for comparison, in CommonJS it looks like this:

const module = require ('module-name')

A module is a JS file that contains one or more values ​​- functions, variables or objects – using the export-Keywords exported. For example like this:


export default str => str.toLowerCase()


However, files are not something that the browser can use right away. Before doing this, he has to convert all these files into data structures. These data structures are called module records. The browser can understand these module records – this intermediate step enables it to find out what a file is all about. In the next step, the module records must be converted to module instances.

Module instance: The code and the state

Such a module instance consists of two things: the code and the state. The code is like a series of instructions. Kind of a recipe for how something should be done. But like when you bake a cake, the recipe alone is not enough for a cake to appear on the birthday table later. You also need ingredients and kitchen utensils for baking. The state gives you these ingredients. It basically describes the actual values ​​of a variable at any point in time. To simplify this, we will fall back on a popular mental model at this point: The variables are only names for the “boxes” in the memory that contain the values. To summarize again: The module instance combines the code (the list of instructions) with the state (all values ​​of a variable). You need a module instance for each module.

As already mentioned, modules are loaded one after the other via the entry point, the import statement. With ES Modules this happens in three steps. The first is to find, download and parse the files into so-called module records. The second is to find the boxes in the memory to which the exported values ​​can be assigned – but they are not yet filled with values. Then comes a process that is also known as linking: This causes both exports and imports to point to the boxes in the memory. In a third step, the code is executed and the boxes are filled with the actual values.

Unlike CommonJS: ES Modules are asynchronous

ES Modules are considered to be asynchronous because this process takes place in precisely these three different phases: loading, instantiating and evaluating – and the three phases can be carried out separately from one another. In contrast to this, in CommonJS modules and their dependencies are loaded, instantiated and evaluated at the same time. Theoretically, this can also run synchronously with ES modules, depending on who carries out the first step – finding, loading and parsing the files. This is because not all tasks in this phase are controlled by the ES module specification. The ES module specification defines how files are parsed into module records and knows how these module records are instantiated and evaluated. However, it does not know how to find the files at all. This is what the loader does. And that is defined in another specification. In the case of browsers, this is the HTML specification. The loader controls exactly how the modules are loaded – it calls the ES module methods Parse.Module, Module.Instantiate and Module.Evaluate. But first he has to find the file with the entry point. About a script-Tag you give him a hint in the HTML where these files can be found:

script src ="main.js" type="module"

The Loader finds all other modules with direct dependencies to main.js via the import statements. They look like this, for example:

import {count} from "./counter.js"

The module specifier – green in the example – tells the loader where to find the next module. However, browsers still only accept URLs as module specifiers. Before ES Modules really lead to an increase in the performance of JavaScript in the browser, it will probably take a while. Support for sharing code of potentially different origins in web packaging formats, for example, is still pending; security issues in this context, along with many other points, are also still unanswered. Exciting future-oriented projects in connection with ESM are, for example, Deno.JS, Pika or Snowpack. Snowpack.js, for example, is based on the premise that ES Modules offer a way to do without bundling tools such as Webpack or Rollup when developing web apps.

You can read more in this informative, very vivid – and cute – illustrated blog post by Lin Clark for the Mozilla blog.

For further reading:

You might be interested in that too

Ready to see us in action:

More To Explore
Enable registration in settings - general
Have any project in mind?

Contact us: