Native JavaScript modules

Serg Hospodarets

About me

Why we need modularity ? πŸ€”

  • code readability
  • separation of concerns
  • localized/atomic changes
  • avoiding global scopes
  • modules/code reuse
  • dependencies management
  • better architecture

Otherwise

First attempt:
external JS files

Pros:
- Code separation
- Separate modules
​

Cons:
- Code reuse

- Global scope

Simple solutions:
- Global vars
- IIFE (immediately-invoked function expressions)

<script src="./vendor/polyfill.js"></script>
<script src="./lib/core.js"></script>
<script src="./components/dropdown.js"></script>
<script src="./components/modal.js"></script>
<script src="./application.js"></script>
// dropdown.js
var delay = 2000; // in ms


// modal.js
var delay = 4000; // in ms

Variable overwrite

// dropdown.js
(function(){
 var delay = 2000; // in ms
 APP.dropdown.delay = delay;
}());
// modal.js
(function(){
 var delay = 4000; // in ms
 APP.modal.delay = delay;
}());

Async Module Definition

Pros:

- Module scope
- Module reuse

- Dependency injection
(testing etc.)

- Tooling: Require.js etc.

Cons:

- Confusing syntax

- Dependencies order because of async nature

- Multiple requests in HTTP 1 era

- Usage with Angular, required additional effort to avoid minification problems

// dropdown.js
define(
    // Module definition
    'components/dropdown',
    // dependencies
    ['vendor/polyfill'
    'lib/core'],
    // module code
    function(core) {
      var dropdown = {};
      dropdown.delay = 2000; // in ms
      return dropdown;
});

// application.js
define([
    'components/dropdown',
    'components/modal',
], function(dropdown, modal) {
  // APP LOGIC
});

CommonJS

Cons:

- No async module load (either load and use as a callback, or implement something with file.read and extracting module.exports)

- Not a standard, not supported in browsers

// dropdown.js
require('../vendor/polyfill');
require('../lib/core');

var dropdown = {};
dropdown.delay = 2000; // in ms
module.exports= dropdown;

// application.js
const dropdown = require('./components/dropdown');
const modal = require('./components/modal')

// APP LOGIC

Pros:

- Supported in Node.js

- Scoping and context

- Modules reuse

- Tooling: Browserify etc.

ECMAScript Modules

aka JavaScript modules, aka ESM

Cons:

- Not supported in browsers?

- Not supported in Node.js?

// dropdown.js
import '../vendor/polyfill.js';
import '../lib/core.js';

export const dropdown = {};
dropdown.delay = 2000; // in ms

// application.js
// STATIC imports
import {dropdown} from './components/dropdown.js';

// Dynamic imports
import('./components/modal.js')
    .then(({modal})=> {
        modal.open();
    });

// OTHER APP LOGIC

Pros:

- Module scopes

- Modules reuse

- Multiple, named exports

- Deferred, buy executed in order

- Both static and dynamic

- ES 2015 standard

- Tooling: Webpack/Babel/Rollup

ECMAScript 2015 (aka ES6) standard

June 2015

~middle 2016

Major browsers and Node.js support ~100% of ES6 features
(NONE included ESM)

Modules ES Spec describes how to parse, instantiate and evaluate modules, but not how to get the files.

ESM implementation timeline πŸ—“

ECMAScript 2015 (aka ES6) standard

June 2015

~middle 2016

Major browsers and Node.js support ~100% of ES6 features
(NONE included ESM)

~fall 2016

HTML spec, which describes how to include, find and load modules

ESM implementation timeline πŸ—“

ECMAScript 2015 (aka ES6) standard

June 2015

~middle 2016

Major browsers and Node.js support ~100% of ES6 features
(NONE included ESM)

~autumn 2016

HTML spec, which describes how to include, find and download modules
First native ES modules implementation in Safari

~end 2016

Support landed in 
MS EDGE, Chrome and Node.js

2017-2018

May 2018

Support landed in Firefox

ESM implementation timeline πŸ—“

ESM Support Today

Table of module systems comparison

Module Scopes

Tools

Static modules

Dynamic loading

Clear syntax

Handy multiple exports

Pure JS

❌

OOB

-

-

βœ…

❌

AMD

βœ…

RequireJS

βœ…

βœ…

❌

β€‹βŒ

CommonJS

βœ…

Browserify

βœ…

❌

β€‹βœ…

❌

ESM

βœ…

Webpack, Babel, Rollup

βœ…

βœ…

βœ…

βœ…

Great, so Webpack/Babel/Rollup do the thing, right? 🧐

Native ESM advantages

Node.js OOB support

Browsers OOB support

Flag for ES6+ features availability

Config NOT required

Build NOT required

Pure JS

❌

βœ…

❌

βœ…

❌

AMD

❌

❌

❌

❌

❌

CommonJS

βœ…

❌

❌

❌

β€‹βŒ

Native ESM

βœ…

βœ…

βœ…

βœ…

βœ…

  • As result- immediate dev cycle, saves time, benefits in debugging and build configs/browser tools

Project structure

Dependencies graph

<script type="module"> takeaways

<script src="./js/classic.js"></script>

/* type="module" is a flag for the browser,
so the script is executed as a module */
<script type="module" src="./js/module.js"></script>

<script type="module"> console.log('inline module'); </script>

1. How to include an ES module script

<script type="module" src="./js/untranspiled-es6+-features.js"></script>

3. Can be used to deliver untranspiled ES6+ features, as old browsers won't execute it (e.g. Babel flag)

statements syntax

(is just a specific "named" export)
export default 60*60;

// or
const secondsInHour=60*60
export { secondsInHour as default };

export.js

import.js

import secondsInHour from './export.js';

// or
import { default as secondsInHour }
    from './export.js';
export const secondsInHour=60*60;
import {secondsInHour} from './export.js';
// Just execute the module
import './export.js';
// Importing the module as an object
import * as lib from './export.js';

lib.secondsInHour; // 3600
lib.secondsInDay=60*60*24; // 86400
// Multiple exports
export const secondsInHour=60*60;
export const secondsInDay=60*60*24;
// Module is not required
// to export something
Named
Default
Just

execute

Multiple

exports

ESM Features

  • Modules have own scopes and contexts, the same time having access to the global scope

  • imports are immutable references and cannot be reassigned

ESM are "singletons"

They are loaded and executed only once

even if imported via different paths or methods

'import' hoisting, top level only

Real Execution Order

Differences with classic scripts

  • always CORS (cross-origin mechanism)

Fix

Modules loading order

  • deferred by default, non-blocking but executed in the order

  • strict by default

Module loading process

  1. Construction β€” find, download, and parse all of the files into module records.

  2. Instantiation β€”find boxes in memory to place all of the exported values in (but don’t fill them in with values yet). Then make both exports and imports point to those boxes in memory. This is called linking.

  3. Evaluation β€”run the code to fill in the boxes with the variables’ actual values.

Credits to Lin Clark 1,2

import {nextEven, isEven} from './evens.js';
import {nextOdd, isOdd} from './odds.js';

// "true" output is expected in all of these expressions
console.log(nextEven(1) === 2);
console.log(nextOdd(1) === 3);
console.log(isOdd(1));
console.log(!isOdd(0));
console.log(isEven(0));
console.log(!isEven(1));
// ./evens.js
import {isOdd} from './odds.mjs';

export function nextEven (n) {
    return isOdd(n) ? n + 1 : n + 2;
}

export function isEven(n) {
    return n % 2 === 0;
}
// ./odds.js
import {isEven} from './evens.mjs';

export function nextOdd(n) {
    return isEven(n) ? n + 1 : n + 2;
}

export function isOdd(n) {
    return !isEven(n);
}

Works because of the "instantiation" phase, which handles circular dependencies between the modules.

ES module path

  • cannot be dynamic for static imports

  • no extensions resolutions

  • no Node.js node_modules etc. resolutions

  • Possible solutions: Service Worker URLs proxy- quite complex

β€’ must be a full URL (include extension)

β€’ must be an absolute URL or:
β€’ must start with β€œ/”, β€œ./”, or β€œ../”

Package name maps
(module resolution algorithm)

<script type="packagemap" src="package-map.json"></script>

<script type="packagemap">
{
  "path_prefix": "/node_modules",
  "packages": {
    "moment": { "main": "src/moment.js" },
    "lodash": { "path": "lodash-es", "main": "lodash.js" }
  }
}
</script>

<script type="module">
    import moment from 'moment';
    import _ from 'lodash';
</script>

Main questions/alternatives:

- should it replicate Node.js module resolution algorithm

- should it follow NPM "main" field resolution

- how to apply for modules in browsers / Node.js and Workers

  • Promise-based API to asynchronously load JavaScript modules
  • Handy usage with async/await
  • Can load ES modules from the classic scripts
  • Can load both module and usual scriptsΒ (in "strict" mode)
  • Provides access to module 'exports'
  • Allows usage of dynamic path and not limited to top level only

Other examples: Code Splitting, polyfills, additional pages, 3rd party scripts, and services etc.

β€’ Under the flag since Node.js 8.5

β€’ Complete polyfill for Node.jsΒ (not fully compliant)Β is available

β€’ Requires .mjs extensions for separation

main

browser

module

Format

CommonJS

CommonJS

ESM

Webpack

βœ…

βœ…

βœ…

Rollup

βœ…

βœ…

βœ…

Other types of scripts

β€’ CSS Houdiny (API to expose browser internals for developers)

Use Worklets- ES modules by default

β€’Β aka Wasm (the new type of high-performant code that can be run in modern web browsers)
in progress of applying ESM

Web/Service Workers

β€’Β TypeScript: some compiler options are available. Open issue for full proper support

Performance

DEMO: Loading ~640 lodash-es native modulesΒ comparing with the bundled version

- allows preloading module script graphs

<link rel="modulepreload" href="dependency.js">
<link rel="modulepreload" href="sub-dependency.js">
...
<script type="module" src="app.js"></script>
<script type="module" src="app.js"></script>
// other ability- for dynamic imports (preloaded but not evaluated)
<link rel="modulepreload" href="modal.js">
<button onclick="import('./modal.js')
    .then(modal => modal.open())">

Unit Testing 🧐

  1. Script
    onload/onerror
    events work for ESM
    Β 

  2. Tests should be deferred

"nomodule" attribute for migration

  1. Add the nomodule attribute for the bundled script

  2. Add type="module" script pointing to the entry file

  3. Resolve import URLs (aliases, '.js' extensions etc.)

  4. Resolve non-JS modules (load dynamically, extract CSS etc.)

  5. Solve node_modules dependencies and their publishing to prod

Proven usage of ESM

β€’ Node.js:

lodash-es

β€’ Web components:
Polymer

β€’ Browser:
hospodarets.com

Native ESM in

production

Native JavaScript modules in production

Conclusions/takeaways

  • Great support

  • How to include: type="module" (browser), .mjs (Node.js)

  • nomodule solution for migration

  • HTTP2 and <link module-preload /> for performance

  • Simplified configuration, debugging and publishing

  • Not fully ready for enterprise projects, perfect for small and middle-size apps

Thank you!

Serg Hospodarets

Native JavaScript modules

By Serg Hospodarets

Native JavaScript modules

All the modern browsers support native JavaScript modules, and it’s a perfect time to start using them, which will change the way we are bundling the JavaScript using Webpack, Rollup, and other bundlers, and how the code is executed. We will take a look how they work, what is the level of support in the browsers and Node.js, plus main findings and gotchas on the way of publishing and using them in production. Looking into examples, we will understand the native modules features, performance details and lazy loading JS modules techniques.

  • 10,681