What are modules?
As our application grows bigger, we want to split it into multiple files, so called “modules”. A module usually contains a class or a library of functions.
📌 A module is just a file. One script is one module.
For a long time, JavaScript existed without a language-level module syntax. That wasn’t a problem, because initially scripts were small and simple, so there was no need.
But eventually scripts became more and more complex, so the community invented a variety of ways to organize code into modules, special libraries to load modules on demand.
Modules can load each other and use special directives export and import to interchange functionality, call functions of one module from another one:
export keyword labels variables and functions that should be accessible from outside the current module.
import allows the import of functionality from other modules.
Advantages of using Modules
Maintainability: Updating a single module is much easier when the module is decoupled from other pieces of code.
Namespacing: In JavaScript, variables outside the scope of a top-level function are global and it’s common to have “namespace pollution”, where completely unrelated code shares global variables. Modules solve that problem. Modules allow us to avoid namespace pollution by creating a private space for our variables.
Reusability: We can use a piece of code where-ever we want in our project by adding modules.
How to incorporate modules?
For instance, if we have a file sayHi.js exporting a function.
// say.js
export function sayHi(user) {
alert(`Hello, ${user}!`);
}
…Then another file may import and use it.
// main.js
import {sayHi} from './say.js';
alert(sayHi); // function...
sayHi('John'); // Hello, John!
The import directive loads the module by path ./sayHi.js relative to the current file, and assigns exported function sayHi to the corresponding variable.
If we want to run the example in-browser, we must tell the browser that a script should be treated as a module, by adding type="module" attribute in script tag.

Core module features
1. “use strict” in modules
Modules always use strict, by default. E.g. assigning to an undeclared variable will give an error.
For example,


2. Module-level scope
Each module has its own top-level scope. In other words, top-level variables and functions from a module are not seen in other scripts.
In the example below, two scripts are imported, and hello.js tries to use user variable declared in user.js, and fails.



Modules are expected to export what they want to be accessible from outside and import what they need.
So we should import user.js into hello.js and get the required functionality from it instead of relying on global variables.
Just like this:


3. A module code is evaluated only the first time when imported
📌 If the same module is imported into multiple other places, its code is executed only the first time, then exports are given to all importers.
That has important consequences. Let’s look at them using examples.
First, if executing a module code brings side-effects, like showing a message, then importing it multiple times will trigger it only once – the first time.
// alert.js
alert("Module is evaluated!");
// Import the same module from different files
// 1.js
import `./alert.js`; // Module is evaluated!
// 2.js
import `./alert.js`; // (shows nothing)

In practice, top-level module code is mostly used for initialization, creation of internal data structures, and if we want something to be reusable – export it.
Let’s look at a bit complex example.
Let’s say, a module exports an object.
// admin.js
export let admin = {
name: "John"
};
If this module is imported from multiple files, the module is only evaluated the first time, admin object is created, and then passed to all further importers.
All importers get exactly the one and only admin object.
// 1.js
import {admin} from './admin.js';
admin.name = "Pete";
// 2.js
import {admin} from './admin.js';
alert(admin.name); // Pete
// Both 1.js and 2.js imported the same object
// Changes made in 1.js are visible in 2.js
So, let’s reiterate – the module is executed only once. Exports are generated, and then they are shared between importers, so if something changes the admin object, other modules will see that.
Such behavior allows us to configure modules on first import. We can set up its properties once, and then in further imports it’s ready.
For instance, the admin.js module may provide certain functionality, but expect the credentials to come into the admin object from outside.

In init.js, the first script of our app, we set admin.name. Then everyone will see it, including calls made from inside admin.js itself.

Another module can also see admin.name.



import.meta
The object import.meta contains the information about the current module.
Its content depends on the environment. In the browser, it contains the url of the script, or a current webpage url if inside HTML.

the keyword this is undefined
That’s kind of a minor feature, but for completeness we should mention it.
In a module, top-level this is undefined.
Compare it to non-module scripts, where this is a global object.


Browser-specific features
Module scripts are deferred
Module scripts are always deferred, same effect as defer attribute (described in the chapter Scripts: async, defer), for both external and inline scripts.
In other words:
downloading external module scripts type="module" src="..." doesn’t block HTML processing, they load in parallel with other resources.
module scripts wait until the HTML document is fully ready (even if they are tiny and load faster than HTML), and then run.
relative order of scripts is maintained: scripts that go first in the document, execute first.
As a side-effect, module scripts always “see” the fully loaded HTML-page, including HTML elements below them.
For example,


Please note: the second script actually runs before the first! So we’ll see undefined first, and then object.
That’s because modules are deferred, so we wait for the document to be processed. The regular script runs immediately, so we see its output first.
When using modules, we should be aware that the HTML page shows up as it loads, and JavaScript modules run after that, so the user may see the page before the JavaScript application is ready.
Some functionality may not work yet. We should put “loading indicators”, or otherwise ensure that the visitor won’t be confused by that.
Async works on inline scripts
For non-module scripts, the async attribute only works on external scripts. Async scripts run immediately when ready, independently of other scripts or the HTML document.
For module scripts, it works on inline scripts as well.
For example, the inline script below has async, so it doesn’t wait for anything. It performs the import (fetches ./analytics.js) and runs when ready, even if the HTML document is not finished yet, or if other scripts are still pending.
<!-- all dependencies are fetched (analytics.js), and the script runs -->
<!-- doesn't wait for the document or other <script> tags -->
<script async type="module">
import {counter} from './analytics.js';
counter.count();
</script>
External scripts
External scripts that have type="module" are different in two aspects:
1. External scripts with the same src run only once:
<!-- the script my.js is fetched and executed only once -->
<script type="module" src="my.js"></script>
<script type="module" src="my.js"></script>
2. If a module script is fetched from another origin, the remote server must supply a header Access-Control-Allow-Origin allowing the fetch. That ensures better security by default.
<!-- another-site.com must supply Access-Control-Allow-Origin -->
<!-- otherwise, the script won't execute -->
<script type="module" src="http://another-site.com/their.js"></script>
No “bare” modules allowed
In the browser, import must get either a relative or absolute URL. Modules without any path are called “bare” modules. Such modules are not allowed in import.
For instance, this import is invalid:
import {sayHi} from 'sayHi';
//Output
Error, "bare" module
the module must have a path, e.g. './sayHi.js' or wherever the module is
Certain environments, like Node.js or bundle tools allow bare modules, without any path, as they have their own ways for finding modules and hooks to fine-tune them. But browsers do not support bare modules yet.
Compatibility, “nomodule”
Old browsers do not understand type=”module”. Scripts of an unknown type are just ignored. For them, it’s possible to provide a fallback using the nomodule attribute.

Sources