ES module preloading and integrity

<! — TOC –>

  • ES module preloading and integrity

    • Modular optimization in production environment
    • Module preloading
    • Module preloading example:
    • The Polyfilling module is preloaded
    • Integrity limitation
    • Call to action
    • JSPM Generator – A module preloaded Generator
  • reference
  • Social information/Social Links:

<! — /TOC –>

Modular optimization in production environment

When using ES modules in a production environment, there are currently two main performance optimizations to apply – code splitting and preloading.

Code split optimizations can be used for native ES modules in a wrapper such as ESbuild or RollupJS. Code splitting ensures that for any two modules that are always loaded together, they will always be inlined to the same module file as a block module for network optimization (or even, if possible, inlined to the entry module itself).

Preloading then solves the problem of modules being loaded in reference order in the module dependency graph – modules are executed only after each module in the static import graph has been loaded, and modules are loaded only after their parent module has been loaded. Reference: Execution sequence of modules before and after introducing top-level await in ESM

Module preloading

The ES module is preloaded in the browser as follows:

<link rel="modulepreload" href="..." />

When it was first released in Chrome, there was a great article about it in the Google Developers 2017 update.

It is recommended to inject the ModulePreload tag as far as possible for all deep dependencies in order to completely eliminate the late cost of module loading, which is also the main benefit of static preloading.

Another major benefit of using ModulePreload is that it is currently the only mechanism to support the full integrity of all loaded modules using the “integrity” attribute. (I don’t understand, wait for me to find information)

For example, app.js loads dependency. Js loads library.js, we could write:

<link rel="modulepreload" href="/src/app.js" integrity="sha384-Oe38ELlp8iio2hRyQiz2P4Drqc+ztA7jb7lONj7H3Cq+W88bloPxoZzuk6bHBHZv"/>

<link rel="modulepreload" href="/src/dependency.js" integrity="sha384-kjKb2aJJUT956WSU7Z0EF9DZyHy9gdvPOvIWbcEGATXKYxJfkEVOcuP1q20GT2LO"/>

<link rel="modulepreload" href="/src/library.js" integrity="sha384-Fwh0dF5ROSVrdd/vJOvq0pT4R6RLZOOvf6k+ijkAyUtwCP7B0E3qHy8wbot/ivfO"/>

<script type="module" src="/src/app.js"></script>

Since preloading causes app.js, dependency. Js, and library.js to be immediately loaded in parallel, module lazy loading is eliminated [and we can fully protect the module execution environment through the integrity of all scripts]. (I don’t understand, wait for me to find information)

Module preloading example:

Code: index. HTML:

<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <! -- <link rel="modulepreload" href="./index.js"/> <link rel="modulepreload" href="./init.js"/> <link rel="modulepreload" href="./a.js"/> <link rel="modulepreload" href="./b.js"/> --> </head> <body> <script src="./index.js" type="module"></script> </body> </html>

index.js:

import './init.js'
import {a, Person, obj, b} from './a.js'

console.log(a, Person, obj, b)


console.log('index')

init.js:

import './b.js'

console.log('init')

export let str = 'init'

const a = 1

export {a}

a.js:

import * as bb from "./b.js"

console.log('b:', bb)

export let a = 1

export function Person() {

}

export const obj = {
    name: "sss"
}

let b = 1

export {b}

b.js:

export let a = 1

export function Person() {

}

export const obj = {
    name: "sss"
}

let b = 1

export {b}

console.log('b')

Unused modules preloaded:

Modules are loaded serially, in the order in which modules are parsed

Use module preloading :(remove index.html code comments)

All modules are loaded in parallel (load time is significantly reduced by 200ms)

However, if the project has a lot of modules, then it is not a good way to add them one by one manually, so it is better to get the entire module dependency graph first, and then automatically add the module preloaded label to the page through the script.

However, again, if the entire module dependency diagram is available, then the entire module dependency is already being loaded. Refer to in-depth ES Module to analyze the principle

The Polyfilling module is preloaded

One problem with this feature is that it is currently only implemented in the Chromium browser, but you can build a polyfill using the following code:

<script>
  function processPreload () {
    const fetchOpts = {};
    if (script.integrity)
      fetchOpts.integrity = script.integrity;
    if (script.referrerpolicy)
      fetchOpts.referrerPolicy = script.referrerpolicy;
    if (script.crossorigin === 'use-credentials')
      fetchOpts.credentials = 'include';
    else if (script.crossorigin === 'anonymous')
      fetchOpts.credentials = 'omit';
    else
      fetchOpts.credentials = 'same-origin';
    fetch(link.href, fetchOpts);
  }
  for (const link of document.querySelectorAll('link[rel=modulepreload]'))
    processPreload(link);
</script>

Also, if we wanted to add dynamic preloading support to this polyfill, we could use Mutant Observer:

new MutationObserver(mutations => { for (const mutation of mutations) { if (mutation.type ! == 'childList') continue; for (const node of mutation.addedNodes) { if (node.tagName === 'LINK' && node.rel === 'modulepreload') processPreload(node); } } }).observe(document, { childList: true, subtree: true });

This does not fill in the Depth Dependency preload, but covers most use cases and enables “integrity” checking in all module browsers by enabling internal integrity mapping. (I don’t understand, wait for me to find information)

In this way, we can get full preloading and integrity support in the production module environment for all browsers.

The polyfill above is included in the latest version 0.12.1 of ES Module Shims, which provides a combination of polyfills for various ES Module functions, especially for importing maps.

Integrity limitation

One major problem with using ModulePreload as the primary integrity method is that there is no simple way to provide up-front integrity for lazily loaded modules without performing preloading immediately. For the current production way of working, the best approach is to build a custom dynamic import wrapper that delays the injection preload until the dynamic import is triggered.

Even with this approach, it is a lot of work, and it can lead to a lot of surprises, and I would be surprised if anyone adopted it. However, module integrity is an absolutely critical feature of a CDN using ES modules.

In the future, possible specifications related to module preloading and integrity include:

  • Import assertions: It is recommended to use Import ‘mod’ assert {integrity: ‘… ‘} syntax, but not yet specified or implemented for import assertions.

    Unfortunately, this feature has the following problem: It removes the main performance advantage of importing the map, which is that it allows all modules in the graph to have longer invalidations that can be cached independently. So while it’s useful for certain specific situations, as a general solution to this problem, it takes a step backwards.

  • Import Map Integrity: I have suggested an Integrity attribute specification for importing maps, allowing them to act as marshalling points here. The difficulty here is getting support from the browser, but so far it hasn’t been successful.
  • Milestones: this is the performance of an experimental method, using the current Proposal Doc and Chrome CL, is designed to allow you to specify some information about the script loading conditions, so that a more granular loading optimization. Unfortunately, there are currently no plans to support this feature for preloading, so this means that it does not address the content integrity issue of the depth graph for delayed module loading in its current form.
  • Lazy Preloads: Another design might be to have an attribute on the preload tag indicating that it should not be preloaded, but its integrity value should still apply. I suggested this on WebAppSec, but the mix of preloading and integrity seems a bit confusing.
  • Web Bundles: From the FAQ, it seems that the current integrity state of Web Bundles is considered a follow-on proposal due to the high bandwidth cost of hash validation.

Perhaps the concept of generic modular integrity could be folded down into a centralized Web integrity list, possibly in combination with other permissions/security features in the security list. Instead, validation can benefit from the broad approach we use to optimize integrity, including merkle trees optimized for block boundaries, and even more exotic things.

Call to action

The basic principle that people who surf the Internet should be able to access sites on the Internet and that all code executed is authenticated by integrity hashes is an absolutely fundamental security attribute.

The best way to do this requires some new work, and we need to actively work with browser vendors and standards bodies to ensure that this security attribute can be completely easily enabled for future Web platforms without friction.

JSPM Generator – A module preloaded Generator

JSPM Generator is a low-level tool that I have been working on to generate import mappings for module CDN or local package paths. Latest versions now include support for static tracing of module dependencies, allowing these preloaded labels to be built for module diagrams. Work is underway to improve these generator APIs and workflows.

JSPM. IO’s online generator also includes support for these features by switching the “Preload” and “Integrity” boxes in the upper right corner of the application.

Here is a demonstration in action to toggle preloading for individual CDN dependencies:

<img src=”https://guybedford.com/jspm-generator-preload.gif” width=”90%”>

reference

  • ES Module Preloading & Integrity