Skip to main content

Modules

Features

ES module is deferred by default

Script downloading affect on html parsing -- v8.dev (Source: link)

Comparison

ES6 vs CommonJS

FeaturesES ModuleCommonJS
Loading styleAsynchronousSynchronous
Exported withLive bindingCached value
Import hanldingLive bindingCached Value
Dependency Resolve TimeBuild timeRuntime
Static Analysis
Tree shaking

ES modules vs classic scripts

  • ES module uses import/export syntax, not available in classis scripts

  • ES modules have strict mode enabled by default, while disabled in classic script

  • modules have a lexical top-level scope

  • this within modules does not refer to the global this but is undefined. (Use globalThis to access the global this.)

    //script.js
    var foo = 1;
    console.log(window.foo); // 1

    // module.mjs
    var bar = 1;
    console.log(window.foo); // undefined
  • ES module is deferred by default

Deep Dive

<script> vs <script type='module'>

Refer to this blog post.

Using JS modules in the browser

  • module fetching is deferred by default, while classic script blocks html parsing

    <!-- type="module" indicates main.mjs is an ES6 module, and only recognized by browsers that support ESM. -->
    <script type="module" src="main.mjs"></script>
    <!-- script with nomodule flag is ignored by browsers supporting ESM, but handled by browsers don't. -->
    <!-- add defer to avoid blocking html parsing -->
    <script nomodule defer src="fallback.js"></script>
  • modules are always evaluated only once, while classic scripts are evaluated whenever encountered.

    <!-- classic.js executes multiple times. -->
    <script src="classic.js"></script>
    <script src="classic.js"></script>

    <!-- module.mjs executes only once. -->
    <script type="module" src="module.mjs"></script>
    <script type="module" src="module.mjs"></script>
    <script type="module">import './module.mjs';</script>
  • modules and their dependencies are fetched with CORS, meaning any cross-origin module scripts must be served with the proper headers, such as Access-Control-Allow-Origin: *.

Why module must be specified explicitly with type='module'?

Great explanation here.

Export difference in ES6 and CJS

In short, ES6 modules exports objects through live binding while CJS modules exports cached values. Exporting binding in ES6 helps dealing with cyclic dependencies, refer to this blog post for in-depth explanation.

  • CommonJS exports cached value

    //index.js
    const foo = require("./foo");

    console.log("Initial external value: ", foo.value); // Initial external value: 0

    console.log("---Increment function call---");
    foo.increment(); // Internal value: 1
    console.log("External value: ", foo.value); // External value: 0

    console.log("---Direct modification from external---");
    foo.value += 1;
    foo.printInternalValue(); // Internal value: 1
    console.log("External value: ", foo.value); //External value: 1

    //foo.js
    let value = 0;

    function increment() {
    value += 1;
    printInternalValue();
    }

    function printInternalValue() {
    console.log("Internal value:", value);
    }

    module.exports = { value, increment, printInternalValue };
  • ES6 exports binding

    //index.js
    import { value, increment, printInternalValue } from "./foo.mjs";

    console.log("Initial external value: ", value); // Initial external value: 0

    console.log("---Increment function call---");
    increment(); // Internal value: 1
    console.log("External value: ", value); // External value: 1

    console.log("---Direct modification from external---");
    value += 1; // TypeError: Assignment to constant variable.
    printInternalValue();
    console.log("External value: ", foo.value);

    //foo.mjs
    let value = 0;

    function increment() {
    value += 1;
    printInternalValue();
    }

    function printInternalValue() {
    console.log("Internal value:", value);
    }

    export { value, increment, printInternalValue };

🔗 References