Skip to content
Lesson 15 of 15

Step 1 of 7 · Reading · ~3 min

Learn

Collections and Property Descriptors

Modules in Depth: Dynamic Imports

JavaScript Fundamentals showed import and export. Here is what happens underneath when modules load, and the tools that go with it: import() to load on demand, top-level await, and how ES modules differ from the older CommonJS system. By the end you can explain when a module's code runs, why loading it twice runs it once, and how asynchronous loading is orchestrated.

Static and dynamic imports

A static import sits at the top level. The whole graph of files it names is found and linked before any code runs, which is what lets tools see your dependencies in advance:

javascript

A dynamic import(path) is an expression. It can appear anywhere, its path can be computed, and it returns a promise of the module's namespace object (all its exports as properties):

javascript

Reach for it to load rarely used code on demand (an admin screen, a heavy chart library), to choose a module by name at run time, or to use an ES module from CommonJS code.

A module runs once

Every module is evaluated at most once, no matter how many files import it. The engine keeps a table from module to its evaluation, and later imports get the same result. Two things follow:

  • Module-level state is shared by everyone: a counter in a module is a singleton.
  • When two imports of the same module overlap in time, the second must wait for the first evaluation, not start another. What is stored in the table is therefore the evaluation in progress, not only its finished result.

Top-level await

In an ES module you may write await at the top level, outside any function:

javascript

The module counts as "still evaluating" until the await settles, and every module that imports it waits as well, while unrelated modules carry on. Use it sparingly: each one delays everything depending on it. Plain scripts (CommonJS), such as your exercises here, do not allow it, so you wrap the work in async function main() and call it.

ES modules versus CommonJS

ES modulesCommonJS
Syntaximport / exportrequire() / module.exports
Loadingstatic analysis first; evaluated asynchronouslyrequire runs the file immediately
Exportslive bindings: importers see later changesa snapshot object
Wherebrowsers and Node (.mjs, or "type": "module")Node (.js by default, or .cjs)
Extrasimport.meta.url, top-level await__dirname, __filename

import.meta.url is the module's own address; derive a directory from it where CommonJS code used __dirname. Node needs the file extension in relative import paths. An ES module can import a CommonJS module, but classically CommonJS reached an ES module only through import(), which is what the dynamic form is also for.

Other points: import "./setup.js" (no names) runs a file only for its side effects; static imports allow tree shaking, where a bundler drops exports nobody imports, while a dynamic import with a computed path cannot be analysed this way, in exchange for code splitting.

Common mistakes

  • Leaving out the .js extension in a Node ES-module import.
  • Using require, __dirname or module.exports in an ES module.
  • Expecting a module's top level to run again on every import.
  • Caching only the finished result of a load, so two simultaneous imports both evaluate it.
  • Using top-level await in a plain script.

Your exercise

The practice runner cannot load real ES modules, so you will build the loader instead: the module table, the waiting, and the dependency order.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…