← シリーズに戻る

993バイト: script 1行を貼ったときに YouTube から落ちてくるすべて

Part 1 / 6 script 1行で配るJS SDKの設計 — 実在5製品を分解する

script タグを1行貼るだけで YouTube の iframe embed API が丸ごと有効になります:

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

レスポンスは非圧縮で 993 バイト、gzip で線上 572 バイトです。埋め込み型JS SDK は全て(Vimeo・Safie・Mux・Video.js・これから自分が作るもの)、この最初のリクエストで同じ問題群を解いています。YouTube の loader はたまたま、端から端まで読めるほど簡潔に解いています。この記事では 993 バイトをブラウザから観測できる HTTP コンテキストとセットで読み、あらゆる script-tag SDK に一般化できる6パターンを取り出します。

測定は 2026-08-08 に fetch したもの(ビルドID 854a788e)です。ビルドIDは回りますが、形は変わりません。

実際に線を流れているもの

リクエストは 1本ではなく 2本で、分割は意図的にそうしています。

リクエストURLサイズCache-Control役割
loaderyoutube.com/iframe_api993 B raw / 572 B gzipprivate, max-age=0本体スクリプトを注入し、グローバルを立ち上げます
本体youtube.com/s/player/854a788e/www-widgetapi.vflset/www-widgetapi.js26.8 KB(1年間 immutable)実際の widget コード

loader の URL は固定で、顧客は HTML に一度貼ったら二度と変えません。本体URLにはビルドID(854a788e)が埋まっているので、YouTube がアップデートを出すたびに loader は新しい URL を指し、キャッシュがまだ更新されていない側でも古いURLは動き続けます。コンテンツを介したリダイレクト、と言ってもいいでしょう。loader そのものがリダイレクトの役割を果たすので、実行中のブラウザは 302 の往復を必要としません。

loader 全文

整形版です(実際に配信されているのは1行につながっています):

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);
  })();
}

これで全部です。パターンは6つ、順番に出てきます。

パターン1: 2URL 分割

scriptUrl が真っ先に宣言されています。loader はこれを指すために存在しています。widget コードを loader 自体に inline しようという試みはありません。loader を小さく(993 B)保つことで単一 TCP 往復に収まり、キャッシュも安く済みます。

単一 URL で widget 全体を流すという代替案は考えられます。ですが YouTube はそうしませんし、iframe を前面に立てているときの Safie も Vimeo も同様です。理由は退屈で、顧客が固定する URL がひとつ、こちらが差し替える URL がひとつ、それだけです。loader は顧客の HTML と交わす固定契約、本体はこちら側のスケジュールで差し替える版付きの成果物、という役割分担になっています。

パターン2: cache-control の非対称

loader の Cache-Control: private, max-age=0 は一見無駄に見えます。毎回再検証するので。ですが無駄ではありません。ETag なしの max-age=0 は、ブラウザに loader を必ず fetch させるので、常に最新のビルドIDを学べます。本体URLにはそのビルドIDが埋め込まれているため、一度取ってしまえばブラウザキャッシュに 1年住み続けられます(max-age=31536000, immutable はハッシュ付きURLの慣行で、YouTube の本体もそう配信されています)。

計算はリピーターに有利に働きます。訪問ごとに 993 バイトの loader(gzipで ~572 B)、本体は 26.8 KB を一度だけ、それから 1年間はゼロ。初回訪問者だけ両方払い、リピーターは 1リクエスト分だけ払います。単一の 27 KB スクリプトを短めのTTLで配る場合と比べると、そちらはキャッシュミスのたびに全体を払わせることになります。

落とし穴は、loader が大きくなると常時再検証コストに食われる点です。YouTube の 993 B なら平気です。30 KB になるなら別の戦略が必要になります。

パターン3: Trusted Types と 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') は CSP のディレクティブで、script.src のような sink に生の文字列を代入するのを禁じます。このポリシー下では a.src = 'https://...' は throw します。名前付きポリシーを通して TrustedScriptURL を返させる必要があります。

youtube.com 自身のレスポンスヘッダには content-security-policy: require-trusted-types-for 'script' が入っています。だから loader がその文脈で走るとき(YouTube 所有ページから読まれた場合、あるいは顧客ページが Trusted Types をオプトインしている場合)、生文字列は名前付きポリシーを通してロンダリングする必要があります。中身の恒等関数(return x)は何もサニタイズしていません。ここでのセキュリティモデルは「このURLは名前付きポリシーから出てきたか?はい/いいえ」であって「このURLは安全か?」ではありません。YouTube は自分の文字列を信じていて、儀式が CSP の要求に応えている、という構図です。

try/catch が効いてきます。window.trustedTypes はほとんどのブラウザに存在しないですし、createPolicy は複数の条件で throw します(名前衝突、禁止された名前)。fallback は「もし何か失敗したら生文字列のまま、ホストページが Trusted Types を強制していないことを祈る」というものです。そのポリシー下にない顧客ページなら生文字列で動きます。

盗むべきパターン: Trusted Types ページで配信しないとしても、URL を包むコスト自体は安いです。顧客の CSP が締まったとき、SDK は無変更で動き続けます。

パターン4: グローバル名前空間ガード

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

二重ロードは起きます。タグマネージャは script タグを複製します。うっかりした統合作業では同じスニペットを2回貼ります。ページ上で誰か別の人がすでに widget をロード済み、ということもあります。ガードは言っています。「YT がすでに存在するなら触るな」。少し下の if (!YT.loading) は一段深い防御です。「loader の前のコピーがすでに始まっているなら、本体スクリプトを2回目に注入するな」。

見た埋め込み型 SDK は全社なんらかの版のこれを持っています。YouTube のガードは定番イディオムです。Vimeo は要素に data-vimeo-initialized を付けます。Mux は customElements.get() で Web Components がすでに存在するかを検出します。Video.js は自分のグローバルを見ます。初日から必要です。持たなかった日は、顧客のタグマネがスニペットを2回発火し、コンソールが重複登録エラーで埋まる日になります。

パターン5: YT.ready() で早着した呼び出しをキューする

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) {}
};

loader が終わった時点で本体スクリプトはまだダウンロードされていません。ですが顧客はこう書きます:

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

YT.ready が普通に動くことを期待して。実際動きます。loader が YT.ready を即座に定義しているからで、本体がロード済みなら callback をすぐ発火し、そうでなければ l に push します。本体スクリプトが到着すると window.onYTReady() が呼ばれ、loaded = 1 をセットしてキューを flush します。

盗むべきは2点です。callback ループ内の try/catch(壊れた callback 1本で残りを止めない)。それから、YT.ready()loader 側で同期的に 定義していること。もしこれが本体スクリプトの中で定義されていたら、本体到着前に呼んだ顧客は未定義関数を踏むことになります。

パターン6: currentScript.nonce の伝播

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

script-src 'nonce-XYZ' 'strict-dynamic' 下では(YouTube 自身のページも、セキュリティ意識の高い顧客ページも張っています)、リクエストの現在の nonce が付いたスクリプトだけが実行を許されます。nonce なしで注入されたスクリプトはブロックされます。strict-dynamic は「信頼されたスクリプトは別のスクリプトを動的に注入してよく、そちらも信頼を継承する」と言っています。

loader は自分の nonce を document.currentScript から読み、注入する本体スクリプトにコピーします。細部が 2つあります。

  • c.nonce は DOM プロパティ、c.getAttribute('nonce') は fallback です。ブラウザによってはセキュリティ上の理由で属性が getAttribute から隠されているがプロパティには残っている、という挙動があるので両方読むのが防御的です。
  • document.currentScript は classic script の中でしか意味を持ちません。バンドラで concat されたスクリプト、あるいは import 経由で読まれたスクリプトからは、loader の元 <script> 要素はここでは見えません。だからこのパターンは、loader が本当にそのまま script タグとして配信されている時にだけ生き残ります。バンドラで再パッケージされると壊れます。

自分の SDK が strict-CSP ページに埋め込まれる可能性があるなら、これがパターンです。nonce をコピーする、防御的に書く、顧客にはバンドラを通さず生の script タグとして配信するよう文書化する、以上です。

パターン7: 古典的な挿入位置

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

Google Analytics 以降、JS SDK は全員これを使っています。どこでも動きます。loader が実行される時点で script が DOM に少なくとも 1本ある(自分自身)ことは保証されています。その手前に挿入すれば、<body> があるかや document.bodynull かを知らずに、可能なら <head> 内の正しい document に入ります。

モダンな代替(document.head.appendChilddocument.body.appendChild)はほぼ動きますが端で崩れます。document.head は非常に早い段階で null のことがありますし、document.body<head> スクリプトしか持たない document では null です。insertBefore(a, b)b を loader 自身にしておくと両方を回避できます。

自分の SDK で盗むべきもの

993バイトのファイル1本に運用面の利点が6つあります:

  1. 本体URLはハッシュで版を切り、loader URL は固定に保つ。顧客が固定する URL がひとつ、こちらが差し替える URL がひとつです。
  2. loader は0秒、本体は1年キャッシュ。リピーターは loader の小さな再検証だけ払います。
  3. script URL は Trusted Types ポリシーで包み try/catch で fallback。今のコストは低く、顧客の CSP が締まったときに本当に効きます。
  4. 二重ロード対策は最初のコミットから。タグマネはスニペットを2回発火します、絶対に。
  5. .ready() キューは loader 側で定義、本体側ではありません。本体到着前に呼ぶ顧客が未定義関数を踏まないようにします。
  6. document.currentScript.nonce を注入 <script> にコピー。厳しい CSP は増える一方なので、これは互換性の当て板になります。

二段分離は他の5パターンが乗る土台です。SDK がモノリシック 1ファイルだと半分できません。分離すれば loader が十分小さくなり、1バイトずつ注釈をつけて読むという営みが成立します。それをやらせてくれるのが YouTube loader で、10分で端から端まで読めます。


これは script-tag JS SDK 5製品(YouTube・Safie・Vimeo・Mux Player・Video.js)分解シリーズの part 1 です。全体目次は シリーズpillar を参照してください。測定日 2026-08-08、ビルドID 854a788e に対する測定です。