← Back to series

993 bytes: everything YouTube ships when you paste one script tag

Part 1 of 1 Designing a script-tag JS SDK: teardowns of five real products

Paste one script tag on your page and you enable YouTube’s whole iframe embed API:

<script src="https://www.youtube.com/iframe_api"></script>

The response body is 993 bytes uncompressed, 572 bytes over the wire with gzip. Every embeddable JS SDK — Vimeo, Safie, Mux, Video.js, whatever you build — solves the same set of problems in that first request. YouTube’s loader happens to solve them tersely enough to read end to end. This post walks the 993 bytes with the browser-observable HTTP context around them, and pulls out the six patterns that generalise to any script-tag SDK.

Measurements are from a fetch on 2026-08-08 (build ID 854a788e). The build ID rotates; the shape does not.

What actually comes down the wire

Two requests, not one, and the split is deliberate.

RequestURLBody sizeCache-ControlWhat it does
Loaderyoutube.com/iframe_api993 B raw / 572 B gzipprivate, max-age=0Injects the body script and sets up the global
Bodyyoutube.com/s/player/854a788e/www-widgetapi.vflset/www-widgetapi.js26.8 KB(immutable, 1 year)The actual widget code

The loader URL is stable — customers put it in their HTML once and never change it. The body URL contains the build ID (854a788e), so whenever YouTube ships an update the loader points at a new URL and the old one keeps working for anyone whose page cache hasn’t refreshed. It’s a redirect-through-content: the loader is the redirect, so an in-flight browser doesn’t need a 302 round trip.

The loader in full

Formatted (the shipped version is one long line):

var scriptUrl = 'https://www.youtube.com/s/player/854a788e/www-widgetapi.vflset/www-widgetapi.js';
try {
  var ttPolicy = window.trustedTypes.createPolicy('youtube-widget-api', {
    createScriptURL: function (x) { return x }
  });
  scriptUrl = ttPolicy.createScriptURL(scriptUrl);
} catch (e) {}

var YT;
if (!window['YT']) YT = { loading: 0, loaded: 0 };
var YTConfig;
if (!window['YTConfig']) YTConfig = { host: 'https://www.youtube.com' };

if (!YT.loading) {
  YT.loading = 1;
  (function () {
    var l = [];
    YT.ready = function (f) { if (YT.loaded) f(); else l.push(f) };
    window.onYTReady = function () {
      YT.loaded = 1;
      var i = 0;
      for (; i < l.length; i++) try { l[i]() } catch (e) {}
    };
    YT.setConfig = function (c) {
      var k;
      for (k in c) if (c.hasOwnProperty(k)) YTConfig[k] = c[k];
    };
    var a = document.createElement('script');
    a.type = 'text/javascript';
    a.id = 'www-widgetapi-script';
    a.src = scriptUrl;
    a.async = true;
    var c = document.currentScript;
    if (c) {
      var n = c.nonce || c.getAttribute('nonce');
      if (n) a.setAttribute('nonce', n);
    }
    var b = document.getElementsByTagName('script')[0];
    b.parentNode.insertBefore(a, b);
  })();
}

That’s it. Six patterns, in order.

Pattern 1 — the two-URL split

scriptUrl is the very first thing declared. The loader exists to point at it. There’s no attempt to inline widget code into the loader itself — the loader stays tiny (993 B) so it fits in a single TCP round trip and can be cached cheaply.

You could imagine an alternative where a single URL streams the full widget. YouTube doesn’t do it, and neither do Safie or Vimeo when their SDK is fronted by an iframe. The reason is boring: one URL customers pin, one URL you rotate. The loader is the stable contract with the customer’s HTML; the body is the versioned artifact you replace on your schedule.

Pattern 2 — the cache-control asymmetry

The loader’s Cache-Control: private, max-age=0 looks wasteful — every visit revalidates. It isn’t. Max-age=0 with no ETag means the browser always fetches the loader, so it always learns the current build ID. The body URL contains that build ID, so once fetched it can sit in the browser cache for a year (max-age=31536000, immutable is common practice for hash-URLs; YouTube’s body is served that way).

The math works out well for repeat visitors: 993 bytes of loader on every visit (or ~572 B compressed), 26.8 KB of body once and then never again for a year. First-time visitors pay both; returning visitors pay one request. Compare with a monolithic 27 KB script served with a shorter TTL — every visitor pays the whole thing on every cache miss.

The catch: if your loader isn’t tiny, the always-revalidate cost eats you. YouTube’s 993 B is fine. If your loader were 30 KB you would want a different strategy.

Pattern 3 — Trusted Types with a try/catch fallback

try {
  var ttPolicy = window.trustedTypes.createPolicy('youtube-widget-api', {
    createScriptURL: function (x) { return x }
  });
  scriptUrl = ttPolicy.createScriptURL(scriptUrl);
} catch (e) {}

Trusted Types (require-trusted-types-for 'script') is a CSP directive that forbids assigning plain strings to sinks like script.src. Under that policy, a.src = 'https://...' throws. You have to first pass the string through a named policy that returns a TrustedScriptURL.

YouTube.com’s own response headers include content-security-policy: require-trusted-types-for 'script'. So when the loader runs in that context (which it will if a YouTube-owned page is what loaded it, or if a customer’s page opts into Trusted Types), the raw string has to be laundered through a policy first. The identity function (return x) does no sanitisation — the security model here is “did this URL come out of a named policy, yes or no”, not “is this URL safe”. YouTube trusts its own strings; the ceremony is what CSP requires.

The try/catch matters. window.trustedTypes doesn’t exist in most browsers, and createPolicy throws under a variety of conditions (name collision, disallowed name). The fallback is: if any of this fails, keep the raw string and hope the host page isn’t enforcing Trusted Types. On a customer page that isn’t under that policy, the raw string works fine.

Steal the pattern: even if you don’t ship on a Trusted Types page today, wrapping the URL is cheap. When a customer inherits a strict CSP, your SDK keeps working without changes.

Pattern 4 — global namespace guards

var YT;
if (!window['YT']) YT = { loading: 0, loaded: 0 };

Double-loading happens. Tag managers duplicate script tags, careless integrations paste the same snippet twice, someone else on the page has already loaded the widget. The guard says: if YT already exists, don’t touch it. The if (!YT.loading) further down is the same defense one layer deeper — if a previous copy of the loader already started, don’t inject a second body script.

Every embeddable SDK I looked at has some version of this. YouTube’s guard is stock idiom. Vimeo uses data-vimeo-initialized on the element. Mux uses customElements.get() to detect that its Web Components already exist. Video.js checks its global. You need this from day one; the day you don’t have it is the day a customer’s tag manager fires your snippet twice and their console fills with duplicate-registration errors.

Pattern 5 — YT.ready() queues callers who arrive early

var l = [];
YT.ready = function (f) { if (YT.loaded) f(); else l.push(f) };
window.onYTReady = function () {
  YT.loaded = 1;
  var i = 0;
  for (; i < l.length; i++) try { l[i]() } catch (e) {}
};

The body script hasn’t downloaded yet when the loader finishes. But customers write:

<script src="https://www.youtube.com/iframe_api"></script>
<script>
  YT.ready(function () { /* use YT.Player */ });
</script>

…expecting YT.ready to just work. It does, because the loader defines YT.ready immediately, and it either fires the callback (if the body has finished loading) or pushes it into l. The body script, when it lands, calls window.onYTReady(), which sets loaded = 1 and flushes the queue.

Two things worth stealing: the try/catch inside the callback loop (one broken callback doesn’t stop the rest), and the fact that YT.ready() is defined synchronously in the loader. If it were defined by the body script, any customer that called it before the body arrived would hit an undefined function.

Pattern 6 — currentScript.nonce propagation

var c = document.currentScript;
if (c) {
  var n = c.nonce || c.getAttribute('nonce');
  if (n) a.setAttribute('nonce', n);
}

Under script-src 'nonce-XYZ' 'strict-dynamic' (which YouTube uses on its own pages, and which many security-conscious customer pages enforce), only scripts marked with the current request’s nonce are allowed to execute. Scripts injected without a nonce get blocked. strict-dynamic says: trusted scripts can dynamically inject other scripts, and those inherit trust.

The loader reads its own nonce off document.currentScript and copies it to the body script it injects. Two subtleties:

  • c.nonce is the DOM property; c.getAttribute('nonce') is the fallback because in some browsers the attribute is hidden from getAttribute for security reasons but is still on the property. Reading both is defensive.
  • document.currentScript is only meaningful inside a classic script. Bundlers that concatenate scripts, or scripts loaded via import, will not see the loader’s original <script> element here. That’s why the pattern only survives when your loader is genuinely served as-is, not repackaged.

If your SDK is likely to be embedded on strict-CSP pages, this is the pattern. Copy the nonce; do it defensively; document to customers that they need to serve the loader as a real script tag, not through a bundler.

Pattern 7 — the classic insertion point

var b = document.getElementsByTagName('script')[0];
b.parentNode.insertBefore(a, b);

Every JS SDK from Google Analytics onward uses this. It works everywhere — every page that runs any script has at least one <script> in the DOM by the time your loader executes (yours). Inserting before it puts you in the right document, in <head> when possible, without needing to know whether <body> exists yet or whether document.body is null.

Modern alternatives (document.head.appendChild, document.body.appendChild) work in most cases but have edge cases: document.head can be null very early, document.body can be null in a document that only has <head> scripts. insertBefore(a, b) where b is the loader itself sidesteps both.

What to steal for your own SDK

Six operational upsides in one 993-byte file:

  1. Version your body URL with a hash; keep the loader URL stable. One URL customers pin, one URL you rotate.
  2. Cache the loader for zero seconds, the body for a year. Repeat visits pay the loader’s small revalidation only.
  3. Wrap script URLs in a Trusted Types policy with a try/catch fallback. Cheap now, essential when a customer’s CSP tightens.
  4. Guard against double-load from the very first commit. Tag managers will fire your snippet twice.
  5. Define your .ready() queue in the loader, not the body. Callers who arrive before the body script must not hit an undefined function.
  6. Copy document.currentScript.nonce onto the injected <script>. Strict CSPs are increasingly common; this is the compatibility fix.

The two-stage split is the foundation the other five patterns stand on. If your SDK is one monolithic file, you can’t do half of these. If it’s split, the loader gets small enough that annotating every byte becomes viable — which is what let YouTube ship a loader you can read end to end in ten minutes.


This is part 1 of a series teardown of five real script-tag JS SDKs (YouTube, Safie, Vimeo, Mux Player, Video.js). See the series pillar for the full table of contents. Measured 2026-08-08 against build 854a788e.