Implementing Dynamic Script Loaders with Integrity

Permalink to "Implementing Dynamic Script Loaders with Integrity"

Part of Dynamic Script Loading Patterns, this page covers exactly one topic: how to build a JavaScript loader factory that programmatically injects <script> elements with correct integrity and crossorigin attributes so the browser’s native SRI verifier runs on every dynamically loaded file.

Quick Reference

Permalink to "Quick Reference"
Attribute Required value Notes
integrity sha384-<base64-digest> Computed on the final minified artifact
crossorigin "anonymous" Required; omitting it causes an opaque response and blocks the script
type "module" or omit Module scripts enforce strict CORS; classic scripts also require CORS transparency for SRI
Browser support All evergreen browsers IE 11 does not support SRI; use feature detection if you must support it

Why a dedicated loader factory is necessary

Permalink to "Why a dedicated loader factory is necessary"

Browsers verify SRI only when two conditions are met: the integrity attribute is set before the element enters the DOM, and the fetch runs in CORS mode so the browser can read the response body. Ad-hoc script injection scattered across a codebase makes it trivial to miss either condition on one injection point. A centralized loader factory enforces both requirements at every call site and couples them to an integrity manifest produced at build time — closing the gap between what was audited at compile time and what actually executes at runtime.

The browser performs the cryptographic comparison; the loader’s job is solely to read the pre-computed digest from the manifest and attach it before DOM insertion. For a deep look at what happens inside the browser during that comparison, see Browser Enforcement & Security Boundaries.

Dynamic script loader data-flow Four boxes connected by arrows: Bundler emits integrity manifest, Loader reads manifest at runtime, createElement sets integrity + crossorigin, Browser verifies hash before execution. Bundler emits manifest Loader reads manifest createElement + integrity attr Browser verifies SHA-384 hash before script executes build time runtime

Canonical implementation

Permalink to "Canonical implementation"

This is a complete, production-ready loader factory. Copy it as-is and replace INTEGRITY_MANIFEST with however your application exposes the build-time manifest (inline JSON blob, a fetched endpoint, or a window.__SRI_MANIFEST__ injection).

// loader.js — integrity-first script loader factory

/**
 * Registry populated from the build-time integrity manifest.
 * Shape: { [publicPath: string]: string }   e.g. { "/app.js": "sha384-abc..." }
 */
const registry = Object.create(null);

/**
 * Populate the registry from a manifest object.
 * Call this before any loadScript() invocations.
 * @param {Record<string, string>} manifest
 */
export function initRegistry(manifest) {
  if (!manifest || typeof manifest !== 'object') {
    throw new Error('[sri-loader] Manifest is missing or malformed — aborting initialisation');
  }
  Object.assign(registry, manifest);
}

/**
 * Inject a <script> element with a verified integrity attribute.
 * @param {string} url  Absolute or root-relative URL matching a manifest key
 * @returns {Promise<void>}
 */
export function loadScript(url) {
  const hash = registry[url];
  if (!hash) {
    return Promise.reject(
      new Error(`[sri-loader] No integrity hash found for "${url}" — refusing to inject`)
    );
  }

  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = url;
    script.integrity = hash;           // e.g. "sha384-abc123..."
    script.crossOrigin = 'anonymous';  // mandatory: enables CORS mode for SRI verification
    script.defer = true;

    script.addEventListener('load', () => resolve());
    script.addEventListener('error', () =>
      reject(new Error(`[sri-loader] Load failed for "${url}" — possible SRI mismatch or network error`))
    );

    document.head.appendChild(script);
  });
}

Usage at application startup:

import { initRegistry, loadScript } from './loader.js';

// manifest.json is emitted by the bundler and served with immutable cache headers
const manifest = await fetch('/integrity-manifest.json').then(r => r.json());
initRegistry(manifest);

await loadScript('/chunks/analytics.abc123.js');
await loadScript('/chunks/vendor.def456.js');

Variant examples

Permalink to "Variant examples" Permalink to "1. Module preloading with <link rel="modulepreload">"

For ES module graphs, modulepreload hints let the browser fetch and verify modules ahead of the import statement. The integrity attribute works identically here.

function preloadModule(url, hash) {
  const link = document.createElement('link');
  link.rel = 'modulepreload';
  link.href = url;
  link.integrity = hash;
  link.crossOrigin = 'anonymous';
  document.head.appendChild(link);
}

// Call early in the critical path, before the dynamic import fires
preloadModule('/modules/checkout.v2.js', 'sha384-XyzAbc...');

2. Webpack 5 manifest integration

Permalink to "2. Webpack 5 manifest integration"

Automating Hash Generation in Webpack 5 shows how webpack-subresource-integrity populates __webpack_require__.sri at build time. You can forward that directly into the registry without a separate manifest fetch:

// After webpack runtime initialises, __webpack_require__.sri contains
// { chunkId: "sha384-..." } entries. Remap to public URLs:
if (typeof __webpack_require__ !== 'undefined' && __webpack_require__.sri) {
  const mapped = {};
  for (const [chunkId, hash] of Object.entries(__webpack_require__.sri)) {
    const url = __webpack_require__.p + __webpack_require__.u(chunkId);
    mapped[url] = hash;
  }
  initRegistry(mapped);
}

3. Parallel loading with Promise.allSettled

Permalink to "3. Parallel loading with Promise.allSettled"

When multiple chunks share no dependency ordering, load them in parallel. Use Promise.allSettled rather than Promise.all so a single SRI failure does not prevent unrelated chunks from loading.

const chunks = [
  '/chunks/sidebar.js',
  '/chunks/tooltips.js',
  '/chunks/analytics.js',
];

const results = await Promise.allSettled(chunks.map(loadScript));

for (const result of results) {
  if (result.status === 'rejected') {
    console.error('[sri-loader] Chunk blocked:', result.reason.message);
    // report to telemetry — do not re-throw; let the app degrade gracefully
  }
}

When a script fails due to an SRI mismatch, pair this pattern with Handling SRI Failures with onerror Handlers to activate versioned fallback endpoints.

Gotchas and edge cases

Permalink to "Gotchas and edge cases"
  • crossOrigin = 'anonymous' is not optional. The browser needs a CORS-transparent response to read the body and compute the comparison hash. Without this attribute the response is opaque, the integrity check cannot proceed, and the browser blocks the script even if the hash is correct. This is the single most common real-world SRI failure.

  • Hash drift from post-build transforms. If a CDN edge function, a reverse proxy, or a service worker modifies the script body after your build computed the hash — even adding a byte-order mark or recompressing with a different deflate dictionary — the hash will not match. Compute hashes on the artifact that the CDN actually serves, and use content-hashed filenames (app.[contenthash].js) to guarantee immutability.

  • Setting integrity after appending to the DOM is too late. Browsers begin fetching as soon as the element enters the DOM. The integrity attribute must be set before appendChild or insertAdjacentElement is called.

  • strict-dynamic removes host allowlists for scripts loaded by trusted parents. When your CSP uses 'strict-dynamic', scripts loaded by a trusted parent script inherit trust propagation — but scripts loaded by loadScript() from untrusted call sites still need an explicit hash or nonce in script-src. Review your Configuring Content-Security-Policy with SRI policy to confirm which call sites are trusted.

  • Fallback URLs must also carry integrity checks. A fallback endpoint that bypasses the registry reintroduces the exact supply chain risk SRI is meant to close. Either serve fallbacks from a trusted local path or pre-register their hashes in the manifest under a separate key.

  • Module scripts and classic scripts behave differently under CORS. type="module" always fetches in CORS mode, so crossOrigin is redundant but harmless. Classic scripts without crossOrigin fetch without CORS and produce opaque responses — integrity verification silently fails.

Verification steps

Permalink to "Verification steps"

Chrome DevTools — confirm SRI is firing:

  1. Open DevTools → Network tab → click the injected script resource.
  2. In the Headers pane, confirm the response includes Access-Control-Allow-Origin.
  3. If the hash is intentionally wrong, the Console shows: Failed to find a valid digest in the 'integrity' attribute for resource 'https://cdn.example.com/app.js' with computed SHA-384 integrity '...'. If you see this with the correct hash, the CDN is mutating the response body.

CLI — verify CORS headers from the CDN:

curl -sI \
  -H "Origin: https://your-app.example.com" \
  "https://cdn.example.com/chunks/analytics.abc123.js" \
  | grep -i "access-control-allow-origin"
# Expected: access-control-allow-origin: *
# or:       access-control-allow-origin: https://your-app.example.com

If the header is absent, SRI will block every dynamically injected script regardless of hash correctness.

CI hash consistency check:

# Recompute SHA-384 of the served artifact and compare against manifest
SERVED_HASH=$(curl -s "https://cdn.example.com/chunks/analytics.abc123.js" \
  | openssl dgst -sha384 -binary | base64 | tr -d '\n')

MANIFEST_HASH=$(jq -r '."/chunks/analytics.abc123.js"' integrity-manifest.json \
  | sed 's/sha384-//')

[ "$SERVED_HASH" = "$MANIFEST_HASH" ] \
  && echo "PASS: hash matches" \
  || echo "FAIL: hash mismatch — CDN is mutating the artifact"

Add this check to your deployment pipeline as a smoke test against each CDN edge region. For broader strategies on locking down which origins your application trusts at all, see Mapping CDN Origins to SRI Policies.


Permalink to "Related"

Related Articles

Adding Integrity to Runtime-Injected Scripts
Dynamic Script Loading Patterns Asset Hashing & Dynamic Script…