Understanding native JavaScript modules


It’s been a long time since the JavaScript projects began to grow in complexity. Because of that, breaking the code into manageable pieces became essential. In the history of JavaScript development, we went through many different approaches to splitting our code into modules. Good examples include solutions such as CommonJS, which is used in NodeJS. Similarly, RequireJS was used in the past with frameworks such as the first Angular.

The JavaScript language received a significant update in 2015 called ES6 or ECMAScript 2015. Among other features, it introduced the official syntax for handling modules. This approach allows us to use the keyword to expose various values from a module. We could then use the statement to access them in other modules.

Unfortunately, when a feature is added to the JavaScript language, it does not automatically mean that the browsers implement it. Because of that, we use tools such as Webpack and Babel to transform the code we wrote that follows the latest standards into code that all browsers can understand. However, the browsers caught up some time ago and introduced native support for ES6 modules. In this article, we explain them and how using them is different from relying on tools such as Webpack.

Loading JavaScript modules

Let’s start by creating a straightforward JavaScript file.


Its job is to find an element with the id and render some text inside. We can now include it in our HTML.


Thanks to using , our JavaScript file is loaded as a module. This affects how the browser treats it.

The execution is deferred

The browser reads the loaded HTML document from top to bottom and parses the section before the . If we put a JavaScript file into the section in a regular way, it will be parsed and executed before the browser parses the section. This would mean that our JavaScript code can’t access the element with the id because it doesn’t exist yet.

However, the execution of JavaScript modules is deferred. It means that the browser executes them when the entire document is parsed. It means that even if we put them into the section, they execute when the element is available.

The CORS policy is applied

For regular tags without , the browser fetches and executes the script even if it comes from a different origin. However, when we load a module, the browser applies the same-origin policy. It prevents our website from accessing modules from other origins. We can adjust this behavior with CORS. Cross-Origin Resource Sharing (CORS) is a mechanism for disallowing or allowing resources to be requested from another origin.

If you want to know what an origin is and how to set up CORS in your Node.js application, check out API with NestJS #117. CORS – Cross-Origin Resource Sharing

This also means that if we open our file directly from our file system, we can see the following error:

Access to script at ‘file:///home/marcin/Documents/Projects/native-modules/index.js’ from origin ‘null’ has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, isolated-app, chrome-extension, chrome, https, chrome-untrusted.

The most straightforward way to deal with it when working on our website locally is to use a library like serve or http-server to make our page available on localhost.

Exporting and importing

Let’s create a simple function and place it in a separate file.


Using the keyword, we can import the above file into .


What’s important, we don’t have to include the file in our . Since we marked as a module, it can import other modules.

Import maps

The path to the module we provide when importing can be relative or absolute. It can also be a URL with a different origin if we correctly configure the Cross-origin Resource Sharing.

Tools such as Webpack can automatically resolve file extensions. However, when using native JavaScript modules, we need to provide the full path to the module. This is why we are including the file extension in our import.

We can add an import map to specify the URLs of the modules we will import. We also have a chance to name them.


Above, we provided the path to in our import map, and called id . Thanks to that, we no longer have to provide the exact path to the file when we import this module.


Dynamic imports

We can avoid importing a particular module until it is required. By not loading large modules upfront, we can help users save mobile bandwidth, for example.

To do that, we can use the function that returns a promise.


Preloading modules

With our current setup, the browser does not start loading the file automatically. Instead, it starts loading it when it encounters the import statement when executing the file.

We can improve the performance by preloading the file ahead of time by adding a element with .


Thanks to that, the and files can be downloaded in parallel.

Handling old browsers

Unfortunately, some older browsers don’t support native JavaScript modules. If our website needs to work on old browsers such as Internet Explorer, we can provide a separate JavaScript file with the property.

By adding , we indicate that this script should not be executed in browsers that support native JavaScript modules.

Native JavaScript modules vs Webpack and similar tools

One of the advantages of using JavaScript modules natively is that they work directly in all modern browsers. This means that we don’t need any tools to build our project. However, we could use imports and exports using tools such as Webpack before the browsers natively supported this feature. It serves a similar purpose but works differently.

Webpack is a bundler. It combines all our JavaScript files and their dependencies into a single file. During this process, Webpack can use various loaders and plugins to transform our code, for example, to include other features not yet supported by the browsers.

The bundle can be split into more than one file to improve the performance. Since the dependencies such as React or Redux don’t change very often, we can bundle them into a separate file that is then cached by the browser.

The most important advantage of Webpack is that it offers many ways to optimize our application. It can minify our code to reduce the file size by removing unnecessary code characters from our code without changing its functionality. It does tree shaking by removing unused code from our bundle that we don’t need to serve to the browser. It can compress assets such as images to decrease the load time of our application.


It’s very good that the browsers follow the JavaScript standard and implement features such as the modules natively. While they definitely have their place, they don’t make tools such as Webpack obsolete. While bundling our code, Webpack has the chance to optimize our application in various ways. What’s interesting is that tools such as Vite use native JavaScript modules. It allows for fast, efficient loading of modules during development. It’s worth knowing how the JavaScript modules work because they might be the future of frontend development. Getting the hang of both traditional tools like Webpack and the approach with native modules can enhance our ability to code effectively.

Notify of
Inline Feedbacks
View all comments