Five lines of code, four ways to break
- 1. The double-load race
- Two independent widgets, a city map and the "next show" radar, both need Leaflet and both add a script tag. The library starts up twice, and the second run wipes out the first: markers end up registered on a map that no longer exists.
- 2. The pre-existing tag
- Some other code on the page loaded the library first. Adding it again causes the same corruption. The loader notices the existing tag and simply reports success.
- 3. The tampered CDN file
- If a CDN is compromised or traffic is intercepted, the file you get isn't the file you tested. With an integrity hash, the browser blocks it before a single line runs. A supply-chain attack on a map library shouldn't become an incident on your site.
- 4. The hung or failed request
- A stalled CDN request mustn't leave a zombie script tag and a promise that never settles. On failure or timeout, the tag is removed and the request is forgotten, so a later retry genuinely tries again.
The fix: one promise per file
The whole design rests on one idea: a small registry, keyed by URL, holds the promise for each file. Whoever asks first starts the download; everyone who asks while it's in flight gets the same promise back. One request, one script tag, one start-up, however many callers there are.
const inFlight = {}; // url -> Promise: the source of truth
function loadScript(url, opts = {}) {
if (inFlight[url]) return inFlight[url]; // 1. concurrent callers share one request
const existing = document.querySelector(`script[src="${url}"]`);
if (existing) return (inFlight[url] = Promise.resolve(existing)); // 2. already on the page
return (inFlight[url] = new Promise((resolve, reject) => {
const s = document.createElement('script');
s.src = url;
if (opts.integrity) { s.integrity = opts.integrity; s.crossOrigin = 'anonymous'; } // 3. the browser verifies it
const fail = err => { s.remove(); delete inFlight[url]; reject(err); }; // 4. clean up so retries work
const timer = setTimeout(() => fail(new Error('timed out')), opts.timeoutMs || 12000);
s.onload = () => { clearTimeout(timer); resolve(s); };
s.onerror = () => { clearTimeout(timer); fail(new Error('failed to load')); };
document.head.appendChild(s);
}));
}
A sketch of the pattern, matching what the demo above runs. The production module is sri-lazy-loader.js on GitHub.
Usage
loadScript('https://unpkg.com/leaflet@1.9.4/dist/leaflet.js', {
integrity: 'sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=',
crossOrigin: ''
}).then(function () {
var map = L.map('map-canvas').setView([40.9, -74.1], 11);
});
| Option | Default | What it does |
|---|---|---|
integrity | — | The SRI hash (sha384-… or sha256-…) for third-party scripts |
crossOrigin | 'anonymous' | Cross-origin mode for scripts with an integrity hash |
timeoutMs | 12000 | How long to wait before giving up and cleaning up |
It returns a promise that resolves with the script element, or rejects with an error on failure or timeout. It never leaves a dead tag or a dangling listener behind.
From the repo's README.
Getting a file's integrity hash
Most CDNs, cdnjs among them, list the SRI hash right next to each file. For any other file, one command produces it:
curl -s https://unpkg.com/leaflet@1.9.4/dist/leaflet.js | openssl dgst -sha384 -binary | openssl base64 -A
Put sha384- in front of the result and that's your integrity value. Pin an exact version in the URL too: a hash only matches one exact file, which is the whole point.
From production
On The DJ Calendar, every city page and the next-show radar map ask for Leaflet the first time a visitor interacts with them. Before this module shipped, the site occasionally ended up with two copies of Leaflet: one map would start up, then a second script tag would land and start the library again, corrupting the first map.
The bug was intermittent and impossible to reproduce on a development machine, because over a local connection everything loads too fast to race. The fix was this one small module, treating the promise registry as the single source of truth.
Design decisions
| Decision | Why |
|---|---|
| A promise registry keyed by URL | Deduping concurrent callers is one line with a cache. Without it, every race is a bug that only appears sometimes. |
| Detect tags already on the page | The page is shared state. Pretending otherwise is how double-initialization bugs ship. |
| SRI on everything third-party | The loader is the last line of defense for supply-chain integrity, and it costs one hash. |
| Clean up on failure | A dead script tag with a dangling error handler is a leak. A timeout must unblock retries, not just reject. |
| No bundler, no framework | One file that a plain script tag or any module system can use. |
Related projects
Frequently asked questions
What does sri-lazy-loader protect against?
It prevents duplicate script loads from concurrent callers, detects an existing script tag, supports Subresource Integrity for third-party scripts, and cleans up failed or timed-out requests.
How does it avoid duplicate script tags?
Callers loading the same URL share the in-flight Promise from a URL-keyed registry, so one script element serves concurrent requests.
What happens when a script load times out?
The Promise rejects with an error, the abandoned script element and listeners are cleaned up, and the in-flight entry is cleared so a later call can retry.
Does sri-lazy-loader need a framework or package install?
No. It has zero dependencies and can be used as a plain JavaScript file. The optional integrity hash lets the browser verify third-party script bytes before execution.
The bugs you never see
Bryan Hamilton is a full-stack web engineer with 25+ years in PHP and JavaScript, available immediately for remote, hybrid or on-site roles in the NYC metro area.