In this article I want to share a translation of an article about native ECMAScript modules , which are more and more discussed among front-tenders. Javascript has never previously supported natively working with modules, and we, the front-tenders, always had to use additional tools for working with modules. But you just imagine that soon you will not need to use Webpack to create bundles of modules. Imagine a world in which the browser will collect everything for you. I want to tell you more about these prospects.In 2016, many interesting features and utilities from new standards were added to browsers and Nodejs, in particular, the
ECMAScript 2015 specification. Now we are faced with a situation where support among browsers is close to 100%:

')
Also,
ECMAScript modules (often called ES / ES6 modules) are actually introduced into the standard. This is the only part of the specification that required and requires the most time to implement, and no browser has yet released them in a stable version.
Recently, Safari 19 Technical Preview and Edge 15 have added the implementation of modules without using flags. The time is already approaching when we can abandon the use of the usual bundles and transfiguration of modules.
To better understand how the world of frontend came to this, let's start with the history of JS modules, and then take a look at the current benefits and implementations of ES6 modules.
A bit of history
There were many ways to connect modules. I will cite as an example the most typical of them:
1. Just a long code inside the script tag. For example:
<!--html--> <script type="application/javascript"> </script>
2. Sharing logic between files and connecting them with script tags:
<!--html--> <script type="application/javascript" src="PATH/module1.js" ></script> <script type="application/javascript" src="PATH/module2.js" ></script>
3. A module as a function (for example: a module function that returns something; a self-invoking function or a constructor function) + Application file / model that will be the entry point for the application:
<!--html--> <script type="application/javascript" src="PATH/polyfill-vendor.js" ></script> <script type="application/javascript" src="PATH/module1.js" ></script> <script type="application/javascript" src="PATH/module2.js" ></script> <script type="application/javascript" src="PATH/app.js" ></script>
To all this, the Frontend community has invented many varieties and new ways that added variety to this holiday of anarchy.
The basic idea is to provide a system that allows you to simply connect a single JS file link, like this:
<!--html--> <script type="application/javascript" src="PATH/app.js" ></script>
But it all came down to the fact that the developers chose the side of the bandlers - code assembly systems. Further it is proposed to consider the main implementations of modules in JavaScript
Asynchronous Module Definition ( AMD )
This approach is widely implemented in the
RequireJS library and in tools such as
r.js for creating the resulting bundle. General syntax:
This is the main format of modules in the Node.js ecosystem. One of the main tools for creating bundles for client devices is
Browserify . The peculiarity of this standard is the provision of a separate scope for each module. This avoids unintended leakage of global variables and global scope.
Example:
ECMAScript modules (aka ES6 / ES2015 / native JavaScript modules)
Another way to work with modules came to us from ES2015. The new standard has a new syntax and features that meet the needs of the frontend, such as:
- individual module scopes
- strict default mode
- cyclic dependencies
- the ability to easily break the code, following the specification
There are many implementations of bootloaders, compilers, and approaches that support one or more of these systems. For example:
Instruments
Today in JavaScript, we are used to using various tools for combining modules. If we are talking about ECMAScript modules, you can use one of the following:
As a rule, the tool provides a CLI interface and the ability to customize the configuration for creating bundles from your JS files. It gets entry points and a set of files. Usually such tools automatically add “use strict”. Some of these tools also know how to translate code to make it work in all environments that are needed (old browsers, Node.js, etc.).
Let's take a look at the simplified WebPack config that sets the entry point and uses Babel for the transporting of JS files:
The config consists of the main parts:
- start with webpack.entry.js
- use Babel louder for all JS files (i.e., the code will be transported depending on the presets / plugins + a bundle will be generated)
- The result is placed in the main.js file.
In this case, as a rule, the index.html file contains the following:
<script src="build/main.js"></script>
And your application uses JS bundles / transpiled code. This is a general approach to working with bundlers, let's see how to make it work in a browser without any bundles.
How to make JavaScript modules work in the browser
Browser Support
Today, every modern browser has support for ES6 modules:

Where can I check
As you have seen, currently you can check out the native JS modules in Safari Technology Preview 19+ and EDGE 15 Preview Build 14342+. Let's download and try the modules in action.
ES modules are available in Firefox
You can download
Firefox Nightly , which means that modules will soon appear in
FF Developer Edition , and then in a stable version of the browser.
To enable ES modules:
- open the `about: config` page
- Click “I accept the risk!”
- find the `dom.moduleScripts.enabled` flag
- double click to change the flag to true
And that's it, now you have ES modules available in Firefox.
Safari Technology Preview with available ES modules
If you are using MacOS, simply download the latest version of Safari Technology Preview (TP) from
developer.apple.com . Install and open it. Starting with
Safari Technology Preview version 21+ , ES modules are enabled by default.
If it is Safari TP 19 or 20, make sure that the ES6 modules are enabled: go to “Develop” → “Experimental Features” → “ES6 Modules”.

Another option is to
download the latest Webkit Nightly and play with it.
EDGE 15 - enable ES modules
You can
download a free virtual machine from Microsoft .
Simply select the “Microsoft EDGE on Win 10 Preview (15.XXXXX)” virtual machine (VM) and, for example, “Virtual Box” (also free) as a platform.
Install and start the virtual machine, then open the EDGE browser.
Go to the about: flags page and enable the Enable experimental JavaScript features flag.

That's all, now you have several environments where you can play with the native implementation of ECMAScript modules.
Differences between native and assembled modules
Let's start with the native features of the modules:
- Each module has its own scope, which is not global.
- They are always in strict mode, even when the use strict directive is not specified.
- A module can import other modules using import directives .
- The module can be exported using export .
Until now, we have not seen any particularly serious differences from what we are used to with the bandlers. The big difference is that the entry point must be provided in the browser. You must provide the script tag with a specific type = "module" attribute, for example:
<script type= "module" scr= "PATH/file.js" ></script>
This tells the browser that your script may contain imports of other scripts, and they must be processed accordingly. The main question that appears here is:
Why can't the JavaScript interpreter define modules if the file is essentially a module?
One of the reasons is that native modules are in strict mode by default, and there are no classic script:
- say, the interpreter analyzes the file, assuming that this is a classic script in a non-strict mode;
- then he finds the "import / export" directive;
- in this case, it must start from the very beginning in order to parse the entire code again in strict mode.
Another reason - the same file can be valid without strict mode and invalid with it. Then validity depends on how it is interpreted, which leads to unexpected problems.
Determining the type of expected file download opens up many ways to optimize (for example, loading imported files in parallel / before parsing the rest of the html file). You can find
some examples used by the Microsoft Chakra JavaScript engines for ES modules .
Node.js way to specify a file as a module
Node.js environment is different from browsers and using the script tag type = "module" is not particularly suitable. The
debate is still ongoing about
how to do this in an appropriate way .
Some decisions were rejected by the community:
- add “use module” to each file;
- metadata in package.json.
Other options are still pending (thanks to
@bmeck for the hint):
- determining if the file is an ES module;
- The new file extension for ES6 Modules .mjs, which will be used as a fallback if the previous version does not work.
Each method has its pros and cons, and at present there is still no clear answer as to which
way Node.js will go .
A simple example of a native module.
First, let's create a
simple demo (you can run it in browsers that you installed earlier to check the modules). So this will be a simple module that imports another and calls a method from it. The first step is to include the file using:
<script type="module"/>
<!--index.html--> <!DOCTYPE html> <html> <head> <script type="module" src="main.js"></script> </head> <body> </body> </html>
Here is the module file:
And finally, the imported utilities:
As you can see, we left the .js file extension when the import directive is used. This is another difference from the behavior of the bandler - native modules do not add .js extensions by default.
Secondly, let's check the scope of the module (
demo ):
var x = 1; alert(x === window.x);
Third, we verify that the native modules are in strict mode by default. For example, strict mode prohibits deleting simple variables.
The following demo shows that an error message appears in the module:
Strict mode cannot be bypassed in native modules.
Total:- The .js extension cannot be omitted;
- the scope is not global, this does not refer to anyone;
- native modules in strict mode by default (you no longer need to write “use strict”).
Built-in module in script tag
Like regular scripts, you can embed code instead of separating them into separate files. In the previous demo, you can simply insert main.js directly into the script type = "module" tag, which
will lead to the same behavior :
<script type="module"> import utils from "./utils.js"; utils.alert(` JavaScript modules work in this browser: https://blog.whatwg.org/js-modules `); </script>
Total:- script type = "module" can be used both for loading and executing an external file, as well as for executing embedded code in the script tag.
How the browser loads and executes modules
Native modules (asynchronous) by default have deffered script behavior. To understand this, we can present each script type = "module" with and without the defer attribute. Here is an image from the specification that
explains the behavior :

This means that by default the scripts in the modules do not block, are loaded in parallel and are executed when the page completes the html parsing. You can change this behavior by adding the async attribute, then the script will be executed as soon as it is loaded.
The main difference between native modules and ordinary scripts is that regular scripts are loaded and executed immediately, blocking html parsing. To present this, look at the
demo with different options of attributes in the script tag , where the usual script without the defer \ async attributes will be executed first:
<!DOCTYPE html> <html> <head> <script type="module" src="./script1.js"></script> <script src="./script2.js"></script> <script defer src="./script3.js"></script> <script async src="./script4.js"></script> <script type="module" async src="./script5.js"></script> </head> <body> </body> </html>
The order of loading depends on the browser implementation, the size of the scripts, the number of imported scripts, etc.
Total:- modules are asynchronous by default and behave like deffered scripts
We are entering the era of native module support in javascript. JS has come a long way of becoming, and finally he got to this point. Probably, this is one of the most awaited and sought-after features. No syntactic sugar and new language constructs are compared to this new standard.
All of the above is given for the first acquaintance with native ECMAScript modules. In the next article, the ways of interaction between modules, the definition of support in browsers, specific moments and differences with ordinary bundles, etc. will be discussed.
If you want to know more now, I suggest to follow the links:
Honestly, when I tried native modules for the first time, and they started working in the browser, I felt something that I didn’t feel with the advent of such language features as const / let / arrow functions and other newfangled chips when they started working directly in browsers . I hope that you will, like me, welcome the addition of a native mechanism for working with modules in browsers.
Other author articles on this topic
From translator
I am a frontend developer in the Avia team at
Tutu.ru. Now we have Webpack in projects as a bandler. There are legacy code and old projects with RequireJS. Native modules are very interesting and we look forward to them, especially since we have already transferred all our projects to HTTP / 2. Of course, we are not going to do without bandlers, since we have a large number of modules in all projects. But the arrival of native modules could change the workflow assembly and deployment.