📑 Contents
The Waterfall Problem Code Splitting modulepreload Tag Integrity Attribute Polyfill (ES Module Shims) Lazy Modules Problem Future Proposals JSPM Generator Mental Model ReferencesES Module Preloading & Integrity
How browsers load ES modules, why there's a waterfall problem, and how to fix it with preloading + integrity checks.
1. The Waterfall Problem
When a browser runs an ES module, it doesn't know what other modules it needs until it starts parsing. This creates a waterfall — each module only gets discovered after its parent finishes loading.
The deeper your module graph (more levels of imports), the worse the delay. This is the core problem that modulepreload solves.
A browser cannot fetch library.js until it has already fetched and parsed both app.js and dependency.js. There is no way to parallelize this without telling the browser in advance.
2. Code Splitting (Background)
Before preloading, bundlers like esbuild and RollupJS address the waterfall differently — by merging modules that are always loaded together into a single chunk. Fewer files = fewer round trips.
| Approach | Mechanism | Trade-off |
|---|---|---|
| No bundling | Each module is a separate file | BAD Many round trips, deep waterfall |
| Full bundle | All code in one big file | OK No waterfall, but can't cache shared code |
| Code splitting | Co-loaded modules merged into chunks | GOOD Fewer files + shared chunks are cacheable |
| Code splitting + preload | Chunks loaded in parallel upfront | BEST No waterfall + cacheable + secure |
Code splitting gets you halfway there. Preloading finishes the job.
3. The modulepreload Tag
The <link rel="modulepreload"> tag tells the browser: "I know you'll need this module — fetch it now, don't wait." All listed modules are fetched in parallel before any of them execute.
<!-- Without preload: browser discovers these one by one -->
<script type="module" src="/src/app.js"></script>
<!-- With preload: browser fetches all 3 in parallel at t=0 -->
<link rel="modulepreload" href="/src/app.js" />
<link rel="modulepreload" href="/src/dependency.js" />
<link rel="modulepreload" href="/src/library.js" />
<script type="module" src="/src/app.js"></script>
You as the developer (or your bundler) know the full module graph ahead of time. You inject the preload tags into the HTML. The browser then fetches everything in one go.
How It Differs from Regular Preload
<link rel="preload"> is for generic resources. modulepreload is specific to ES modules — the browser processes it through the module system (parse + compile), so there's no "double parse" when the module is actually imported.
4. The Integrity Attribute
The integrity attribute lets you specify a cryptographic hash of the file. The browser verifies the downloaded file matches before executing it. If someone tampers with the file on a CDN, the browser refuses to run it.
<link rel="modulepreload"
href="/src/app.js"
integrity="sha384-Oe38ELlp8iio2hRyQiz2P4Drqc+ztA7jb7lONj7H3Cq+W88bloPxoZzuk6bHBHZv" />
<link rel="modulepreload"
href="/src/dependency.js"
integrity="sha384-abc123..." />
Why it matters: This is called Subresource Integrity (SRI). It protects against supply-chain attacks — if a CDN is compromised and serves a modified file, the hash won't match and the browser blocks execution entirely.
What Makes modulepreload Special for Security
modulepreload is currently the only mechanism that lets you attach integrity checks to every module in your dependency chain. A normal <script type="module"> tag can have an integrity attribute, but its dynamically imported dependencies cannot — unless they are also preloaded.
| Method | Integrity Support |
|---|---|
<script integrity="..."> |
YES — entry point only |
import './mod.js' (static import) |
NO — no place to put hash |
import('./mod.js') (dynamic import) |
NO — no place to put hash |
<link rel="modulepreload" integrity="..."> |
YES — any module in the graph |
5. The Polyfill — ES Module Shims
At the time of writing (2021), modulepreload only worked in Chromium browsers (Chrome, Edge). Firefox and Safari didn't support it. So a polyfill was needed.
The polyfill is included in ES Module Shims v0.12.1+. It intercepts <link rel="modulepreload"> tags and fetches them using the fetch() API with integrity support, making it work in all browsers.
How the Polyfill Works
-
1Scans the page for
<link rel="modulepreload">tags -
2Fetches each one using
fetch(url, { integrity, credentials, referrerPolicy }) -
3Reads the response fully via
.arrayBuffer()— this prevents a double-fetch race condition where the browser might fetch the same file twice -
4Uses a
MutationObserverto watch for dynamically added preload tags too -
5Stores the fetched module in cache so when the
<script type="module">tag runs, it uses the cached version
Usage
<!-- Load polyfill FIRST, before any modulepreload tags -->
<script async src="https://ga.jspm.io/npm:es-module-shims@1.x/dist/es-module-shims.js"></script>
<link rel="modulepreload" href="/src/app.js" integrity="sha384-..." />
<link rel="modulepreload" href="/src/dependency.js" integrity="sha384-..." />
<script type="module" src="/src/app.js"></script>
6. The Lazy Module Problem
Everything above works great for modules that load at startup. But modern apps use lazy loading — loading code only when the user navigates to a specific page or clicks a button.
// This module is loaded lazily on user action
button.addEventListener('click', async () => {
const { doSomething } = await import('./heavy-feature.js')
doSomething()
})
You can't put a modulepreload tag in your HTML for heavy-feature.js because that would defeat the purpose of lazy loading — the file would be fetched immediately on page load.
This creates a gap: lazy-loaded modules cannot get integrity protection without preloading them upfront. And preloading them upfront kills the performance benefit of lazy loading.
The Core Tension
| Goal | Approach | Problem |
|---|---|---|
| Performance | Lazy load (don't preload) | No integrity verification possible |
| Security | Preload with integrity hash | Kills lazy-loading performance benefit |
This is an unsolved problem at the time of the article — and motivates several future proposals.
7. Future Proposals
Several ideas are being explored to solve the lazy module + integrity problem. None have full browser support yet.
A. Import Assertions (Inline Integrity)
// Proposed syntax — hash inline with the import
import './heavy-feature.js' assert { integrity: 'sha384-...' }
Problem: This breaks import maps. Import maps let you remap bare specifiers to URLs — if the hash is on the import statement, different parts of the app using different hashes for the same module causes conflicts. The caching and deduplication benefits of import maps are lost.
B. Import Map Integrity
// Hash lives in the import map, not the import statement
<script type="importmap">
{
"integrity": {
"/src/heavy-feature.js": "sha384-..."
},
"imports": {
"heavy-feature": "/src/heavy-feature.js"
}
}
</script>
Why this is better: The import map acts as a single source of truth for all module hashes. Any import of the module — from any part of the app — uses the same hash. No duplication, no conflict. This proposal is the most promising but was not yet browser-adopted at time of writing.
C. Lazy Preloads
<!-- Conceptual: attach integrity without triggering an actual fetch -->
<link rel="modulepreload" href="/src/heavy.js" integrity="sha384-..." lazy />
The idea: register the integrity hash for a module without actually fetching it yet. When the browser eventually loads it (via dynamic import), it checks against the pre-registered hash. This is still a design concept — not a real spec yet.
D. Web Bundles
A separate proposal to bundle an entire site into a single signed bundle. Integrity is handled at the bundle level. However, verification involves hashing the entire bundle — expensive for large apps — and integrity for lazy modules within a bundle is considered a follow-up problem.
8. JSPM Generator
JSPM (JavaScript Package Manager) is a tool that can automatically generate modulepreload tags with integrity hashes by tracing your module graph statically.
What It Does
- Takes your entry point module(s) as input
- Traces all static imports recursively
- Generates
<link rel="modulepreload">tags with correct integrity hashes for every discovered module - Outputs ready-to-paste HTML
Online Generator
The JSPM Online Generator (at generator.jspm.io) lets you toggle preload and integrity generation via checkboxes. Useful for quickly getting the right tags for a given package or module graph.
Practical tip: In a real project, your build tool (Vite, Rollup, esbuild) should generate these tags automatically during production build. Manually managing integrity hashes for every module is not sustainable. Look for build plugins that emit modulepreload tags into your HTML.
Mental Model — Put It All Together
The key insight: modulepreload gives you both performance (parallel loading, no waterfall) and security (hash verification) at the same time. It's not a trade-off — you get both. The only remaining gap is lazy-loaded modules, which is an active area of standards work.
Quick Cheat Sheet
| Concept | One-liner |
|---|---|
| Waterfall | Modules discovered one by one as each parent loads — adds latency |
| Code splitting | Merge co-loaded modules into chunks to reduce file count |
| modulepreload | Tell browser all needed modules upfront → parallel fetch, no waterfall |
| integrity hash | Browser verifies file matches expected hash before running — blocks tampered code |
| ES Module Shims | Polyfill that makes modulepreload + integrity work in non-Chromium browsers |
| Lazy module problem | Dynamic imports can't get integrity unless preloaded, which defeats the purpose |
| Import map integrity | Proposed fix: store all hashes in the import map as a single source of truth |
References
-
1ES Module Preloading & Integrity — Guy BedfordThe primary article this study page is based on. Covers modulepreload, integrity, polyfill, and future proposals in depth.
-
2MDN: rel="modulepreload"Official browser API documentation for the modulepreload link relation.
-
3MDN: Subresource Integrity (SRI)Explains the integrity attribute, how hashes are computed (sha256/sha384/sha512), and browser verification behaviour.
-
4ES Module Shims — GitHubThe polyfill library that adds modulepreload + integrity support to non-Chromium browsers.
-
5JSPM Online GeneratorTool for generating modulepreload tags with integrity hashes from a module graph. Has toggles for preload and integrity output.
-
6Chrome Developers: JavaScript module preloadingChrome team's explainer on how modulepreload works internally and performance impact.
-
7WHATWG HTML Spec: modulepreloadThe formal specification for the modulepreload link type.