← Learn に戻る

JavaScript SDK 設計: 実在5製品のバンドル分解

基板に並んだ電子部品を背景にした、シリーズ「JavaScript SDK 設計 — 実在5製品のバンドル分解」のカード

動画プレイヤーやカメラ映像、チャットを顧客のサイトに script 1行で載せる SDK を作るとき、最初に決めるのは埋め込み形態です。顧客の CSS や CSP とぶつけたくない、扱うデータが機密なら iframe 型で版は選ばせない。顧客が自分で UI を組むなら DOM 直挿しで版を選ばせる。YouTube・Safie・Vimeo・Mux Player・Video.js の配信バンドルを 2026-08-08 に取得して読み、loader の分け方、CSP、postMessage、版、初期化まで6つの決断で並べます。サイズとヘッダは実測、挙動は配信 JS を読んだもので、最終章の骨格コードは動かしていません。

読み終えるとできること

  • 埋め込み形態から先に選べます。顧客の CSS や CSP と干渉させたくない、データが機密なら iframe 型にして版は選ばせず、顧客が UI を組むなら DOM 直挿しにして、版番号まで完全に指定 (exact version) できる URL を顧客に選ばせます
  • YouTube の 993 バイトの loader から、二重ロード対策や currentScript.nonce の伝播を含む7パターンを自分の loader に移せます
  • iframe の中を、Safie と同じ per-request nonce の CSP と connect-src の限定で固められます
  • postMessage を、origin の等値比較と event.source の照合で、同じページに2箇所埋め込まれても混線しない契約にできます
  • 6つの決断を1つずつ選んだ最小の埋め込み SDK の骨格を読み、自分の要件に置き換えられます

顧客のサイトに、動画プレイヤーやカメラ映像、チャットの窓を載せたい。npm パッケージで配ると、導入できるのはビルドできる技術者のいる顧客に限られますが、CDN に置いて <script> を1行貼ってもらう形なら、CMS を使う顧客でも HTML に貼るだけで動きます。導入の手間がゼロに近いことが採用の条件になる領域では、この配り方しか選べません。

ただし、その1行の裏では、埋め込み形態、loader と本体の分け方、CSP、postMessage の契約、版とキャッシュ、初期化の入口という6つの決断が済んでいて、しかも配ったコードは顧客のページの中で、顧客のスクリプトと同じ権限で動きます。

顧客の CSS や CSP とぶつけたくない、扱うデータが機密なら iframe 型にして、版は選ばせません。顧客が自分で UI を組む、フレームワークに溶け込ませたいなら DOM 直挿しにして、版番号まで完全に指定 (exact version) できる URL を顧客に選ばせます。 埋め込み形態を決めた時点で、版の方針と postMessage の要否までほぼ決まります。実在の5製品を並べると、iframe 型の YouTube・Safie・Vimeo は3製品とも顧客に版を選ばせておらず、DOM 直挿し型の2製品はどちらも postMessage を使わずに版を顧客に選ばせていて、Video.js は URL に版を必須にし、Mux Player は版を書けば1年 immutable でキャッシュされます。

同じ script 1行でも、描画する場所で2つに分かれることを5製品で並べた図。iframe 型の Safie は同じ iframe.html を4枚開いて hash で役割を分け、safie.link の iframe に UI 一式を置く。YouTube は本体を顧客のページに差し込み、置き場所に iframe を作って youtube.com の iframe に描画する。Vimeo は顧客が書いた iframe に後から付き、要素なら、その中に iframe を作って Vimeo の iframe に描画する。この3製品は自社のオリジンで描画し、顧客の CSS・JS は届かない。DOM 直挿し型の Mux Player は mux-player をカスタム要素として定義し、Video.js は顧客が書いた video 要素をプレイヤーに変える。この2製品は顧客の DOM の中で描画する。配信 JS は 2026-08-08 取得 (Safie と Vimeo は 09-30 に一部を取り直して確認)

残りの決断は、形態ほど一斉には割れません。loader と本体を分けるかは版を自社の都合で切り替えたいかどうかで決まり、初期化の入口は顧客が静的 HTML を書くのか SPA を書くのかに合わせます。CSP と postMessage をどこまで重くするかは、扱うデータの機密性と親子で共有する状態の量で決まると私は読んでいます。二重ロード対策だけは5製品中4製品が入れていて、割れる決断というより最初から入れておくものです。

このシリーズでは5製品の配信バンドルを取ってきて読み、決断ごとに章を立てています。分解が先で、抽象は後です。最終章で、分解で見た6つの決断を選び直して最小の埋め込み SDK を組みます。

揃える課題

比較が意味を持つのは、5製品に同じ仕事をさせたときだけです。

  • 仕事: 顧客のページに <script> を1行貼ってもらい、映像を再生する UI を出す (Safie はカメラ映像、残りの4製品は動画)
  • 起点: 顧客が貼る URL (loader)。そこから読み込まれるファイルを本体まで追う
  • 取得: 2026-08-08 に curl で認証なしで取得。サイズは HTTP ボディ、gzip は実際に線上を流れる量
  • 読むもの: 配信された JS とレスポンスヘッダ (公式ドキュメントは根拠にしない)
  • 取り直し: Safie の鍵交換、Vimeo の待ち合わせ、二重ロード対策の有無は 2026-09-30 に取り直して読み直した

揃えたのは仕事と読み方、揃っていないのはデータの機密性、という比較の条件を5製品の列で並べた図。仕事 (script 1行で映像を再生する UI を出す。Safie はカメラ映像、残り4製品は動画)、起点 (顧客が貼る URL の loader から、読み込まれるファイルを本体まで追う)、取得 (2026-08-08 に curl、認証なし。一部を 09-30 に取り直し)、読むもの (配信された JS とレスポンスヘッダ。公式ドキュメントは根拠にしない) の4行は全製品共通。扱うデータの行だけが揃っておらず、Safie は録画映像へのアクセス権、YouTube と Vimeo は公開動画、Mux Player と Video.js は本文では比べていない。この差がマトリクスの CSP と postMessage の列に出ていると私は読んでいる (推論)

題材を映像に揃えたので、差が出たらそれは題材より設計の違いから来たものと読めます。揃っていないのは扱うデータの機密性です。Safie は録画映像へのアクセス権を含み、YouTube が流すのは誰でも見られる公開動画なので、この差が postMessage と CSP の列に出ていると私は読んでいます。

どの SDK が一番いいかの順位は付けません。5製品は違う制約に合わせて作られていて、見たいのは制約から決断への対応のほうです。ドキュメントを読まずにバンドルを読むのは、ドキュメントが API の表面を書くのに対して、バンドルは1リクエストで実際に何が届くかを示すからです。この2つはときどき食い違います。

顧客のページで動く JS の量と、iframe の中で動く JS の量を、サイズに比例した棒で5製品並べた図 (2026-08-08 に curl で実測した HTTP ボディ)。iframe 型の Safie は顧客のページに loader 160 KB、iframe の中に本体 2.61 MB。YouTube は loader 993 B と本体 26.8 KB が顧客のページで動き、iframe の中は未計測。Vimeo は player.js 24.4 KB が顧客のページで動き、iframe の中は未計測。DOM 直挿し型の Mux Player 1.06 MB と Video.js 690 KB は単一ファイルで、全部が顧客のページで動く。YouTube の本体は loader が顧客のページに差し込むので、顧客のページ側に数えた

5つの製品

製品埋め込むもの顧客が貼る URLひとことで
Safieカメラ映像safie.link/sdk/js/api/v1/latest/160 KB の loader から、2.61 MB の本体を iframe に隔離する
YouTube動画youtube.com/iframe_api993 バイトの loader が、ビルド ID 付きの本体を差し込む
Vimeo動画player.vimeo.com/api/player.jsiframe 型だが単一ファイル。顧客が書いた iframe に後から付けるか、要素の中に iframe を作る
Mux Player動画cdn.jsdelivr.net/npm/@mux/mux-player全部入りの Web Component として顧客の DOM に入る
Video.js動画vjs.zencdn.net/8.23.4/video.min.js顧客の <video> 要素をプレイヤーに変えるライブラリ

上の3つが iframe 型、下の2つが DOM 直挿し型です。

5製品 × 6つの決断

5製品×6つの決断のマトリクス。行は Safie・YouTube・Vimeo (iframe 型) と Mux Player・Video.js (DOM 直挿し型)、列は形態・loader と本体・CSP・postMessage の世代・版・初期化。黄色く塗った postMessage と版の2列は、iframe 型の3製品が「世代1〜3」「版を選ばせない」、DOM 直挿し型の2製品が「使わない」「版を選ばせる / exact 必須」と、形態でそのまま割れている。Vimeo の CSP は未確認

製品形態loader と本体CSPpostMessage の世代版初期化
Safieiframe (4枚を hash で役割分け)分離 160 KB → 2.61 MBiframe 内で per-request nonce、connect-src を自社に閉じる3 暗号化まで (syn → ack → connected、親 → 子を AES-GCM)選ばせない (/latest/ を no-cache)命令的 (namespace)。二重ロード対策は見当たらない
YouTubeiframe分離 993 B → 26.8 KBloader が Trusted Types と nonce の伝播で顧客の CSP に合わせる2 状態同期 (listening → initialDelivery、origin は等値比較)選ばせない (loader は max-age=0、本体はビルド ID 付き URL)命令的 (callback)。window.YT で二重ロードを防ぐ
Vimeoiframe (顧客が書いた iframe に後付けするか、要素の中に SDK が作る)単一 24.4 KB未確認1 最小 (ping → ready、初回の origin を固定し event.source も照合)選ばせない (版なし URL を max-age=1800)命令的 (constructor)。要素に初期化済みの印
Mux PlayerDOM 直挿し (Web Components)単一 1.06 MB顧客の CSP の下で動く (構造上。個別には未確認)使わない選ばせる (@3.7.0 は1年 immutable)Web Components。customElements.get で二重定義を防ぐ
Video.jsDOM 直挿し (<video> 要素)単一 690 KB顧客の CSP の下で動く (構造上。個別には未確認)使わないexact 必須 (/8/ は 403)data 属性。プレイヤー化済みの要素を飛ばす

postMessage と版の2列は、形態の列とそのまま同じ線で割れます。iframe 型の3製品はどれも顧客に版を選ばせず、DOM 直挿し型の2製品はどれも postMessage を使わずに版を顧客に選ばせています。

なぜ版の方針が形態にここまで沿うのかは、バンドルには書かれていません。ここからは私の推論です。iframe 型は親 (顧客のページ) と子 (自社オリジンの iframe) が postMessage で話すので、プロトコルの版を親子で揃え続ける必要があり、顧客に古い loader を貼りっぱなしにされるとその整合が崩れます。DOM 直挿し型には親子の会話が無く、代わりに API の変更が顧客のコードを壊すので、アップグレードの時期を顧客が選べる形に落ち着いたのだと考えています。

CSP と postMessage の世代が形態の中でさらに割れるのは、扱うデータの機密性の差から来ていると私は読んでいます。公開動画の YouTube と Vimeo は暗号化を入れず、録画映像へのアクセス権を扱う Safie は iframe の中に per-request nonce の CSP を張り、親から子への命令を AES-GCM で暗号化しています。Video.js はライブラリなのでセキュリティを顧客の側に置き、Mux は全部入りの Web Component として顧客の DOM に入る代わりに隔離を持ちません。

表の列が見ているもの

表の列に並べた6つが、比べる物差しです。

決断何を見るか詳しい章
形態自社オリジンの iframe に描画するか、顧客の DOM に直接入れるか。隔離と引き換えに、全部の呼び出しが postMessage を通るようになります第2・4章
loader と本体顧客が貼る URL (loader) と、実際の機能 (本体) を別のファイルに分けるか。分けると、顧客の URL を変えずに本体だけ差し替えられます第1章
CSP顧客のページの CSP の下で動けるか、iframe の中で自分の CSP を張れるか。張れるなら、乗っ取られたときの持ち出し先まで絞れます第1・2章
postMessage の世代親子の会話をどこまで重くするか。1 最小 (準備完了を待つだけ)、2 状態同期 (初期状態を流す)、3 暗号化まで、の3つに分けています第3章
版顧客に版を選ばせるか、自社で切り替えるか。loader と本体の cache-control が、この方針をそのまま表します第4章
初期化顧客が SDK を呼ぶ入口。命令的、data 属性、Web Components の3種と、同じ SDK が2回読まれたときの守り方第5章

形を決める2つ

script 1行 SDK の設計軸。iframe 隔離型 / DOM 直挿し型 と loader 分離 / 単一ファイルの4象限に5製品を配置した図。左上に Safie と YouTube、左下に Vimeo、右下に Mux Player と Video.js、右上の DOM 直挿し×分離だけが空

6つのうち、SDK の外形を決めるのは形態と loader の分け方の2つです。iframe 型を選ぶと分けたくなります (顧客のページには postMessage の橋渡し役、iframe の中には UI 一式が要る) が、Vimeo は iframe 型でも player.js 1本で配っていて、このサンプルでは例外です。右上の象限は空いています。DOM 直挿しで分けても、顧客の URL を固定したまま本体を差し替えるうまみが薄く、追加のリクエストだけが残るからだと私は読んでいます。

形態の選択で何が変わるかは、顧客のページで起きることを並べると見えます。

顧客のページでDOM 直挿しiframe 隔離
CSS顧客の CSS に上書きされるフレーム内で完結する
JS顧客の JS から書き換えられるSame-Origin Policy が分ける
Cookie顧客の Cookie 空間を共有する自分のオリジンに閉じる
CSP顧客の CSP に従うフレーム内で自分で張り直せる
払う代償なし描画で +1 リクエスト、全メソッド呼び出しが postMessage RPC

<script src="…"> を顧客が書いた瞬間、そのスクリプトは顧客のページで XSS と同じ権限を持ちます。iframe に隔離すると親の DOM・Cookie・フォームには触れなくなりますが、フレームの中から外へ出ていくリクエストは止まらず、postMessage で event.origin を検証しなければ攻撃側の iframe から命令を送れるので、隔離を選んでもプロトコル層で気を抜けば同じ穴が空きます。これが第2・3章の中心です。

iframe が止めるのは親子の手出しまでで、外への通信と偽の命令は止めないことを、iframe 隔離型の SDK を置いた顧客のページ (example.com) で示した図。顧客の CSS・JS・Cookie から自社オリジンの iframe (子) への矢印は Same-Origin Policy の壁で止まり、届かない。iframe の側からも親の DOM・Cookie・フォームに触れない。iframe の中では UI 一式が動き、CSS はフレーム内で完結し、CSP は自分で張り直せる。代償として、loader (親) からの全部の呼び出しが postMessage を通る。止まらないものは2つで、フレームの中から外部のサーバーへ出るリクエストと、event.origin を検証しないときに攻撃側の iframe から送った命令。描画で +1 リクエスト増えるのも隔離の代償で、DOM 直挿しなら顧客の CSS・JS・Cookie の3本はすべて届く

自分ならどれか

最終章「埋め込みコードを自作する: 最小 SDK を6決断で組む」の末尾にある表と同じものです。

状況選ぶもの効いている理由
顧客サイトの CSS・JS・Cookie と干渉させたくないiframe 型。loader と本体を分け、版は選ばせない顧客のスタイルや window.fetch の書き換えが届かない。loader を毎回取り直させれば、親子の版がずれない
顧客が自分で UI を組む、フレームワークに溶け込ませたいDOM 直挿し (Web Components)。版番号まで指定できる URL を顧客に選ばせる版を書いてもらえば、API が変わってもアップグレードの時期を顧客が選べる (Video.js は版が必須、Mux Player は書けば1年 immutable)
認証トークンや録画映像のように、漏れると困るものを扱うiframe の中に per-request nonce の CSP を張り、connect-src を自社に閉じる乗っ取られても外へ持ち出せない (Safie)。静的ホスティングだけでは組めない
親子で状態を共有する子の準備完了の合図を待ってから、初期状態を流すYouTube の listening → initialDelivery
同じページに2箇所以上埋め込まれうるorigin の等値比較に加えて event.source を照合する2つの iframe は origin が同じなので、origin だけでは自分の iframe か区別できない
顧客が JS を書かない静的 HTML や CMS初期化は data 属性Video.js の data-setup のように、貼るだけで動く
顧客の環境が静的 HTML と SPA に分かれている初期化は 命令的 + 遅延実行キューロード前の呼び出しを配列に積み、ロード後に巻き戻すので、どちらでも同じ書き方で動く
どの形でも二重ロード対策を最初のコミットから入れるタグマネージャはスニペットを2回発火する。5製品中4製品が入れている

上の2行で形態が決まると、版の方針もそこで決まります。残りの行は、選んだ形態の中で何を足すかの判断で、機密性と顧客の書き方が決め手になります。

形態を選ぶと版が決まり、残りは状況で足す、という選ぶ順の図 (本文「自分ならどれか」の表を並べ直したもの。最終章の結論と同じ)。顧客サイトの CSS・JS・Cookie と干渉させたくないなら iframe 型で、loader と本体を分け、版は選ばせない。この形態の中で足すものは、漏れると困るものを扱うなら per-request nonce の CSP を iframe の中に張り connect-src は自社に閉じる、親子で状態を共有するなら子の準備完了の合図を待ってから初期状態を流す、同じページに2箇所以上なら origin の等値比較に加えて event.source を照合する。顧客が自分で UI を組む、フレームワークに溶け込ませたいなら DOM 直挿し (Web Components) で、版番号まで指定できる URL を顧客に選ばせる。iframe も親子の会話も無いので左の3行は当たらない (5製品の DOM 直挿し型は postMessage を使わない)。どちらの形態でも、JS を書かない静的 HTML や CMS なら初期化は data 属性、静的 HTML と SPA に分かれているなら命令的 + 遅延実行キュー、タグマネージャは2回発火しうるので二重ロード対策を最初のコミットから入れる。5製品中4製品が二重ロード対策を入れている

このシリーズの検証状態

サイズとヘッダは実測、挙動は配信された JS を読んだもので、最終章のコードは動かしていません。

章実測 (curl で取ったサイズ・ヘッダ)ソース読み (配信 JS を grep・読解した挙動)未実行
この入口のページ5製品の loader と本体のサイズ (Vimeo 24.4 KB、Mux Player 1.06 MB、Video.js 690 KB を含む。2026-08-08 に curl で実測)マトリクスの CSP・postMessage・初期化のセル (詳しくは第2〜5章)―
1 loader と本体YouTube の loader 993 B / gzip 572 B、本体 26.8 KB、cache-controlloader の7パターン (Trusted Types、二重ロード対策、currentScript.nonce の伝播 等)―
2 iframe 隔離Safie の loader 160 KB、本体 2.61 MB の内訳、iframe.html の CSP ヘッダ4つの iframe の役割分け、SYN/ACK と AES-GCM の鍵の渡し方 (2026-09-30 に取り直して確認)―
3 postMessage―3製品のメッセージ形式・origin 検証・ハンドシェイク (Vimeo の待ち合わせは 2026-09-30 に取り直して確認)―
4 版と cache-control5製品の cache-control、Video.js /8/ の 403――
5 初期化―3つの初期化スタイル、二重ロード対策 5製品中4製品 (2026-09-30 に取り直して確認)―
6 自作――loader と iframe 本体の骨格コード。ブラウザで動かしていない

表に入っていない推論の中心は2つで、iframe 型が版を選ばせない理由 (親子のプロトコルを揃え続けるため) と、CSP と postMessage の重さが機密性で割れる理由 (Safie は録画映像へのアクセス権を扱うため) です。どちらもバンドルには書かれていないので、本文ではこの2つと、空いている象限の理由を「私の推論です」「と私は読んでいます」と断って書いています。Vimeo の CSP と、Mux Player・Video.js が顧客の CSP の下でどう振る舞うかは、このシリーズでは読んでいません。

本文の主張を確かさの段で5つに並べた図。上ほど確か。実測 (curl で取った値) は5製品の loader と本体のサイズ、cache-control、Safie の iframe.html の CSP ヘッダ、Video.js /8/ の 403 で、2026-08-08 に取得。ソース読み (配信 JS を読んだ挙動) は YouTube の loader の7パターン、Safie の4つの iframe と鍵交換、3製品の postMessage、初期化3種と二重ロード対策で、Safie の鍵交換・Vimeo の待ち合わせ・二重ロード対策は 09-30 に取り直した。私の推論 (バンドルに無い理由) は iframe 型が版を選ばせない理由、CSP と postMessage の重さが機密性で割れる理由、DOM 直挿し×分離の象限が空いている理由で、本文では「私の推論です」「と私は読んでいます」と断っている。読んでいない (このシリーズの外) のは Vimeo の CSP と、Mux Player と Video.js が顧客の CSP の下でどう振る舞うかで、マトリクスでは「未確認」「構造上。個別には未確認」。動かしていない (書いただけ) のは最終章の loader と iframe 本体の骨格コードで、ブラウザで動かしていない

読む順

このページのあとに6つの章が続きます。順に読むと論がつながりますが、疑問が1つに絞れているなら、その章から読んでも構いません。

疑問が1つなら、その章から読んでよいことを示す章の地図。第1〜5章で5製品を分解し、第6章で6つの決断を選び直す。第1章は「loader に何を入れるか」で YouTube を分解し、loader と本体・CSP を扱う。第2章は「iframe の中をどう固めるか」で Safie、形態・CSP。第3章は「親子の会話をどこまで重くするか」で Vimeo・YouTube・Safie、postMessage。第4章は「顧客に版を選ばせるか」で5製品、版・形態。第5章は「顧客が SDK を呼ぶ入口は」で5製品、初期化。5つの章はすべて第6章 自作に流れ込み、チャットウィジェットを題材に6つの決断を1つずつ選び直して最小の埋め込み SDK の骨格を書く。骨格コードはブラウザで動かしていない

測定日は2026-08-08時点です。ビルドID と実測バイト数は動きますが、設計空間の形は動きません。