Designing a script-tag JS SDK: teardowns of five real products
Paste one script tag on a page and something loads. Behind that one line is a choice between five different architectures, and the choice you make there constrains everything else — how you version, how you isolate, how you handshake, whether the customer’s CSP will let you run.
This series reads five real production SDKs — the bundles you actually download when a <script src="…"> fires — and pulls out the design axes they disagree on. It’s teardown-first: fetch the file, read the code, measure the response. The goal is that by the end you could ship your own.
When a script tag is the only option
Some products have to run inside someone else’s website: video players, camera feeds, chat widgets, payment forms, maps. Ship them as an npm package and you’ve limited your customers to teams that can build. Ship them as a CDN URL plus one <script> tag and they run anywhere HTML can be pasted, including CMS installs and non-engineer customers. In markets where “trivial to install” is a hard requirement, no other distribution shape competes.
The CDN part is easy: low-latency edges (servers the CDN places physically close to end users worldwide), cacheable responses, versionable URLs. The harder part is that the code you shipped now runs inside the customer’s page with the same privileges as the customer’s own code. That’s where the interesting design starts.
Why iframe
If you inject your script directly into the customer’s page, their CSS overrides yours, their JS can rewrite yours, and you’re sharing their cookie jar. Your button colors drift, your internal state leaks, your auth tokens mingle with theirs.
Rendering inside an <iframe> under your own origin (player.vimeo.com, www.youtube.com/embed/...) isolates all four at once: CSS is scoped to the frame, JS is separated by the same-origin policy, cookies bind to your origin, and you set your own CSP inside. It’s the cheapest way to draw a boundary the browser enforces for you.
The cost is two things. The iframe adds a request, and every method call has to go through postMessage RPC. “Play the video” becomes an async round trip.
What a script tag hands to the customer
The moment the customer writes <script src="…">, your script has XSS-equivalent power on their page: read the whole DOM, read cookies, intercept forms, fire requests behind their back. Even if you write it honestly, a CDN takeover or dependency compromise ships straight to their users.
Iframe isolation reduces this structurally. Whatever runs inside the frame can’t touch the parent’s DOM or cookies. But if you skip event.origin checks on postMessage or send with *, an attacker frame can drive your API from the outside. Isolation without protocol discipline reopens the same hole.
DOM-direct SDKs (Mux Player, Video.js) give up isolation because being in the customer’s DOM is a feature: inherit their CSS as a Web Component, expose a build-your-own UI. They assume the customer trusts their script.
Five products, five compromises
Distribution ease (<script> in one line), isolation (iframe vs direct injection), and protocol-layer security pull in different directions. No solution maximizes all three, so the five products in this series draw the line in different places. Safie runs surveillance cameras, so they pay for iframe plus protocol encryption. Video.js is a library, so security is the customer’s job. Mux takes the “Web Component with everything included” position and swallows the trade-off whole. Reading how each of them drew the line is the interesting part; the teardowns follow.
The five under the microscope
| Product | Loader URL | Loader (raw / gzip) | Body | Version-pinnable? |
|---|---|---|---|---|
| Safie | safie.link/sdk/js/api/v1/latest/ | 160 KB / 37 KB | iframe, 2.61 MB | ❌ (latest in URL) |
| YouTube | youtube.com/iframe_api | 993 B / 572 B | www-widgetapi.js 26.8 KB | via hash-URL body only |
| Vimeo | player.vimeo.com/api/player.js | 24.4 KB / 7.9 KB | single file | ❌ (unversioned) |
| Mux Player | cdn.jsdelivr.net/npm/@mux/mux-player | 1.06 MB / 292 KB | single file | ✅ (npm semver) |
| Video.js | vjs.zencdn.net/8.23.4/video.min.js | 690 KB / 201 KB | single file | ✅ (required — /8/ returns 403) |
Measured 2026-08-08. Sizes are the actual HTTP body; gzip is the on-the-wire cost. All bundles were fetched with plain curl, no auth.
The two axes that everything hangs off
Iframe vs DOM-direct. Safie, YouTube, and Vimeo (in embed mode) render inside an <iframe> under their own origin. Mux Player and Video.js render into the customer’s DOM as Web Components / regular elements. Iframe isolation gives you free CSS containment, a self-served CSP, and cross-origin script boundaries. It costs you a postMessage RPC layer for every method call.
Loader/body split vs single file. Safie’s loader is 160 KB and the real code is a 2.61 MB iframe body. YouTube’s loader is 993 B and the body is 27 KB. Video.js and Mux ship one file with no split.
These two decisions map together but not perfectly. If you go iframe, you almost certainly split (the customer’s script needs the postMessage shim; the iframe body needs the UI code). If you go DOM-direct, splitting buys you less and costs you an extra request. But the version-pinning behaviour follows the iframe/DOM axis strictly: every iframe-based product in this sample refuses to let customers pin a version. The reason is not laziness — it’s that iframe SDKs have to keep parent/child protocol versions in sync, and forcing customers off the treadmill of upgrades would fracture that.
Reading order
The series is six spokes plus this pillar. Read in order for the argument; jump to a spoke when you have the specific question:
- Part 1: 993 bytes, byte by byte — YouTube’s loader is small enough to read end to end. Six patterns any script-tag SDK reuses.
- Part 2: iframe isolation (upcoming) — how Safie moves 2.6 MB of Angular into an iframe from a 160 KB parent, with its own CSP.
- Part 3: postMessage contracts (upcoming) — origin handshakes, JSON Schema vs
.d.ts, and when you actually need AES-encrypted messages. - Part 4: versioning and cache-control (upcoming) — the iframe/DOM split shows up starkly here.
- Part 5: initialization styles and double-load survival (upcoming) — imperative,
data-attributes, custom elements. All five have been double-loaded in the wild. - Part 6: build your own (upcoming) — a minimal embeddable SDK end to end, with the six decisions from the teardowns made explicit.
What this series is not
It’s not a rank-order of which SDK is “best”. Each of the five is calibrated for a different set of constraints — Safie needs authenticated real-time video with per-user rotating keys; Video.js is a library for people who want to write their own player; Mux ships a batteries-included Web Component. The interesting object of study is the design axes themselves, not the point on each axis that any one product chose.
It’s also not a walk through public docs. Everything measured here was pulled from the actual response bodies. Docs describe the API surface; the bundle describes what you get when you make one HTTP request. Those are different documents and they disagree in interesting places.
Measurements as of 2026-08-08. Build IDs and exact byte counts will drift; the shape of the design space will not.
Series chapters
- 0 Designing a script-tag JS SDK: teardowns of five real products Pillar (you are here)
- 1 993 bytes: everything YouTube ships when you paste one script tag YouTube's iframe embed API delivers a 993-byte loader that then fetches the real 27KB widget. Reading the loader byte by byte reveals six patterns every embeddable SDK reuses — Trusted Types, nonce propagation, ready-queueing, and a two-tier cache.
Was this article helpful?