Loading Modules

While ECMAScript 6 defines the syntax for modules, it doesn’t define how to load them. This is part of the complexity of a specification that’s supposed to be agnostic to implementation environments. Rather than trying to create a single specification that would work for all JavaScript environments, ECMAScript 6 specifies only the syntax and abstracts out the loading mechanism to an undefined internal operation called HostResolveImportedModule. Web browsers and Node.js are left to decide how to implement HostResolveImportedModule in a way that makes sense for their respective environments.

Using Modules in Web Browsers

Even before ECMAScript 6, web browsers had multiple ways of including JavaScript in an web application. Those script loading options are:

  1. Loading JavaScript code files using the <script> element with the src attribute specifying a location from which to load the code.
  2. Embedding JavaScript code inline using the <script> element without the src attribute.
  3. Loading JavaScript code files to execute as workers (such as a web worker or service worker).

In order to fully support modules, web browsers had to update each of these mechanisms. These details are defined in the HTML specification, and I’ll summarize them in this section.

Using Modules With <script>

The default behavior of the <script> element is to load JavaScript files as scripts (not modules). This happens when the type attribute is missing or when the type attribute contains a JavaScript content type (such as "text/javascript"). The <script> element can then execute inline code or load the file specified in src. To support modules, the "module" value was added as a type option. Setting type to "module" tells the browser to load any inline code or code contained in the file specified by src as a module instead of a script. Here’s a simple example:

  1. <!-- load a module JavaScript file -->
  2. <script type="module" src="module.js"></script>
  3. <!-- include a module inline -->
  4. <script type="module">
  5. import { sum } from "./example.js";
  6. let result = sum(1, 2);
  7. </script>

The first <script> element in this example loads an external module file using the src attribute. The only difference from loading a script is that "module" is given as the type. The second <script> element contains a module that is embedded directly in the web page. The variable result is not exposed globally because it exists only within the module (as defined by the <script> element) and is therefore not added to window as a property.

As you can see, including modules in web pages is fairly simple and similar to including scripts. However, there are some differences in how modules are loaded.

I> You may have noticed that "module" is not a content type like the "text/javascript" type. Module JavaScript files are served with the same content type as script JavaScript files, so it’s not possible to differentiate solely based on content type. Also, browsers ignore <script> elements when the type is unrecognized, so browsers that don’t support modules will automatically ignore the <script type="module"> line, providing good backwards-compatibility.

Module Loading Sequence in Web Browsers

Modules are unique in that, unlike scripts, they may use import to specify that other files must be loaded to execute correctly. To support that functionality, <script type="module"> always acts as if the defer attribute is applied.

The defer attribute is optional for loading script files but is always applied for loading module files. The module file begins downloading as soon as the HTML parser encounters <script type="module"> with a src attribute but doesn’t execute until after the document has been completely parsed. Modules are also executed in the order in which they appear in the HTML file. That means the first <script type="module"> is always guaranteed to execute before the second, even if one module contains inline code instead of specifying src. For example:

  1. <!-- this will execute first -->
  2. <script type="module" src="module1.js"></script>
  3. <!-- this will execute second -->
  4. <script type="module">
  5. import { sum } from "./example.js";
  6. let result = sum(1, 2);
  7. </script>
  8. <!-- this will execute third -->
  9. <script type="module" src="module2.js"></script>

These three <script> elements execute in the order they are specified, so module1.js is guaranteed to execute before the inline module, and the inline module is guaranteed to execute before module2.js.

Each module may import from one or more other modules, which complicates matters. That’s why modules are parsed completely first to identify all import statements. Each import statement then triggers a fetch (either from the network or from the cache), and no module is executed until all import resources have first been loaded and executed.

All modules, both those explicitly included using <script type="module"> and those implicitly included using import, are loaded and executed in order. In the preceding example, the complete loading sequence is:

  1. Download and parse module1.js.
  2. Recursively download and parse import resources in module1.js.
  3. Parse the inline module.
  4. Recursively download and parse import resources in the inline module.
  5. Download and parse module2.js.
  6. Recursively download and parse import resources in module2.js

Once loading is complete, nothing is executed until after the document has been completely parsed. After document parsing completes, the following actions happen:

  1. Recursively execute import resources for module1.js.
  2. Execute module1.js.
  3. Recursively execute import resources for the inline module.
  4. Execute the inline module.
  5. Recursively execute import resources for module2.js.
  6. Execute module2.js.

Notice that the inline module acts like the other two modules except that the code doesn’t have to be downloaded first. Otherwise, the sequence of loading import resources and executing modules is exactly the same.

I> The defer attribute is ignored on <script type="module"> because it already behaves as if defer is applied.

Asynchronous Module Loading in Web Browsers

You may already be familiar with the async attribute on the <script> element. When used with scripts, async causes the script file to be executed as soon as the file is completely downloaded and parsed. The order of async scripts in the document doesn’t affect the order in which the scripts are executed, though. The scripts are always executed as soon as they finish downloading without waiting for the containing document to finish parsing.

The async attribute can be applied to modules as well. Using async on <script type="module"> causes the module to execute in a manner similar to a script. The only difference is that all import resources for the module are downloaded before the module itself is executed. That guarantees all resources the module needs to function will be downloaded before the module executes; you just can’t guarantee when the module will execute. Consider the following code:

  1. <!-- no guarantee which one of these will execute first -->
  2. <script type="module" async src="module1.js"></script>
  3. <script type="module" async src="module2.js"></script>

In this example, there are two module files loaded asynchronously. It’s not possible to tell which module will execute first simply by looking at this code. If module1.js finishes downloading first (including all of its import resources), then it will execute first. If module2.js finishes downloading first, then that module will execute first instead.

Loading Modules as Workers

Workers, such as web workers and service workers, execute JavaScript code outside of the web page context. Creating a new worker involves creating a new instance Worker (or another class) and passing in the location of JavaScript file. The default loading mechanism is to load files as scripts, like this:

  1. // load script.js as a script
  2. let worker = new Worker("script.js");

To support loading modules, the developers of the HTML standard added a second argument to these constructors. The second argument is an object with a type property with a default value of "script". You can set type to "module" in order to load module files:

  1. // load module.js as a module
  2. let worker = new Worker("module.js", { type: "module" });

This example loads module.js as a module instead of a script by passing a second argument with "module" as the type property’s value. (The type property is meant to mimic how the type attribute of <script> differentiates modules and scripts.) The second argument is supported for all worker types in the browser.

Worker modules are generally the same as worker scripts, but there are a couple of exceptions. First, worker scripts are limited to being loaded from the same origin as the web page in which they are referenced, but worker modules aren’t quite as limited. Although worker modules have the same default restriction, they can also load files that have appropriate Cross-Origin Resource Sharing (CORS) headers to allow access. Second, while a worker script can use the self.importScripts() method to load additional scripts into the worker, self.importScripts() always fails on worker modules because you should use import instead.

Browser Module Specifier Resolution

All of the examples to this point in the chapter have used a relative module specifier path such as "./example.js". Browsers require module specifiers to be in one of the following formats:

  • Begin with / to resolve from the root directory
  • Begin with ./ to resolve from the current directory
  • Begin with ../ to resolve from the parent directory
  • URL format

For example, suppose you have a module file located at https://www.example.com/modules/module.js that contains the following code:

  1. // imports from https://www.example.com/modules/example1.js
  2. import { first } from "./example1.js";
  3. // imports from https://www.example.com/example2.js
  4. import { second } from "../example2.js";
  5. // imports from https://www.example.com/example3.js
  6. import { third } from "/example3.js";
  7. // imports from https://www2.example.com/example4.js
  8. import { fourth } from "https://www2.example.com/example4.js";

Each of the module specifiers in this example is valid for use in a browser, including the complete URL in the final line (you’d need to be sure ww2.example.com has properly configured its Cross-Origin Resource Sharing (CORS) headers to allow cross-domain loading). These are the only module specifier formats that browsers can resolve by default (though the not-yet-complete module loader specification will provide ways to resolve other formats). That means some normal looking module specifiers are actually invalid in browsers and will result in an error, such as:

  1. // invalid - doesn't begin with /, ./, or ../
  2. import { first } from "example.js";
  3. // invalid - doesn't begin with /, ./, or ../
  4. import { second } from "example/index.js";

Each of these module specifiers cannot be loaded by the browser. The two module specifiers are in an invalid format (missing the correct beginning characters) even though both will work when used as the value of src in a <script> tag. This is an intentional difference in behavior between <script> and import.