タグアーカイブ TC39

JavaScriptの隔離実行を実現するShadowRealm APIとCSS設計への影響

JavaScriptの隔離実行を実現するShadowRealm APIとCSS設計への影響

TC39で策定が進む「ShadowRealm」APIはJavaScriptに新たな隔離実行の仕組みを持ち込む。グローバルスコープを汚染しないサンドボックス環境でコードを動かせるためサードパーティライブラリやテストコードの管理が大きく変わる可能性がある。

CSS設計の観点からもこのAPIは注目に値する。CSS-in-JSの実行や外部スクリプトによるスタイル競合といった問題に対してレルム(領域)レベルの分離が使えるようになればフロントエンド全体の堅牢性が一段上がるからだ。

本記事ではShadowRealmの基本的な考え方から具体的なAPIの使い方、そしてCSS設計との接点までを整理する。

JavaScriptのスレッドとレルム(領域)の基本

JavaScriptのスレッドとレルム(領域)の基本
従来の理解(誤解を含む)

「JavaScriptはシングルスレッド言語である」

より正確な捉え方

「ひとつのレルム(領域)はシングルスレッドだがJavaScriptアプリケーションは複数のレルムをまたいでマルチスレッド実行できる」

※Web Workersやiframeは別のレルムに属し専用のスレッドで動く。

上の図はよくある「JSはシングルスレッド」という言説が誤解を生む部分を示している。実際にはブラウザのタブひとつがひとつのレルムでありその中でJavaScriptはメインスレッドで動く。一方Web Workersは別のレルムでワーカースレッドを使いiframeもまた独自のレルムを持つ。

シングルスレッド言語の誤解

「JavaScriptはシングルスレッド」というのは「言語仕様としてマルチスレッドを提供していない」という意味では正しい。関数単位で別のスレッドにオフロードするといった仕組みはない。だがWeb Workersを使うとJavaScriptを別スレッドで実行できる。

ここで混乱が生まれる。言語がシングルスレッドでもアプリケーション全体ではマルチスレッド実行が可能だからだ。CSS-Tricksの記事ではこの点を「JavaScriptのレルムはシングルスレッド」と整理している。つまり実行環境の単位であるレルムに着目すれば言語の特性とアプリケーションの振る舞いを矛盾なく説明できる。

レルムとは何か

レルムとはコードが実行される環境そのものを指す。ブラウザタブが持つグローバルオブジェクト(window)や組み込みオブジェクト(ArrayObjectなど)はすべてそのレルムに紐づく。同じページ内のiframeであっても別のレルムでありwindowArrayも別物だ。

たとえば親ページで定義したグローバル関数はiframe内のレルムからは見えない。逆も同様だ。こうした隔離は意図しない干渉を防ぐ一方でレルム間の連携にはpostMessageなど特定の手段が必要になる。

グローバルスコープ汚染の現実とCSSへの影響

グローバルスコープ汚染の現実とCSSへの影響
Before(隔離なし)
広告スクリプトA
window.themeColor = '#ff0000';
自社スクリプトB
document.body.style.color = window.themeColor;
※意図しない赤色が適用される
After(ShadowRealmで隔離)
広告スクリプトA(ShadowRealm内)
globalThis.themeColor = '#ff0000';
自社スクリプトB(メインレルム)
document.body.style.color = 'initial';
※影響を受けない
■ 広告スクリプトA ■ 自社スクリプトB ※破線枠はShadowRealm内部を表す

JavaScriptのグローバルスコープは簡単に汚染される。変数のvar宣言の巻き上げや不用意なグローバル変数の追加に加えてサードパーティの広告タグやアナリティクスツール、A/Bテスト用スクリプトが暗黙のうちにグローバル空間へ値を書き込む。これがCSSにまで波及することがある。

サードパーティスクリプトがもたらすCSSの衝突

広告配信スクリプトがwindow.themeColorを書き換えればそれを参照している自前のCSS-in-JSロジックが意図しないスタイルを適用してしまう。また外部ウィジェットがページ全体のfont-sizeを動的に変更すればレイアウトが崩れる原因になる。

こうした問題はグローバルスコープを共有するからこそ起こる。完全に隔離されたレルムで外部コードを動かせれば変数の上書きやオブジェクトの改変は発生せず結果としてCSSの意図しない変化も防げる。

CSS-in-JSにおける隔離の必要性

CSS-in-JSライブラリではスタイルの計算結果をJavaScriptのオブジェクトや変数として管理するケースが多い。テーマ切り替えや動的スタイル生成にグローバル変数を使っていると外部スクリプトがそれらを上書きするリスクがある。ShadowRealmを使ってスタイル計算を隔離すれば外部からの干渉を受けない安全なスタイル生成が可能になる。

ShadowRealm APIの仕組み

ShadowRealm APIの仕組み

TC39で策定中のShadowRealmはコードを独立したグローバル環境で実行するためのAPIだ。このAPIには大きく分けてふたつの機能がある。evaluate()で任意のJavaScript文字列を評価する方法とimportValue()でモジュールを動的インポートしてエクスポートされた値だけを安全に受け取る方法だ。

基本的な使い方

// ShadowRealmインスタンスを作成
const shadow = new ShadowRealm();

// 外側のレルムでグローバル関数を定義
function globalFunction() {}

console.log( globalThis.globalFunction );
// 結果: function globalFunction()

// ShadowRealm内で同じ関数を評価しても未定義
console.log( shadow.evaluate( 'globalThis.globalFunction' ) );
// 結果: undefined
外側のレルム(メインスレッド)
globalThis.globalFunction → function globalFunction()
ShadowRealm内(同じメインスレッド上)
globalThis.globalFunction → undefined
※ShadowRealmは独自のグローバルオブジェクトと組み込みオブジェクトを持つがスレッドは共有する。

コードが示すようにShadowRealmインスタンス内部のグローバル環境は外側から完全に切り離されている。しかもこのコードはメインスレッド上でそのまま実行されるためスレッド間通信のコストや複雑さを伴わない。

CSS-Tricksの記事ではこの性質を「無限に使える使い捨てのクリーンルーム」と表現している。テストのモックデータが本番のグローバル変数と衝突する心配もなくサードパーティコードを安全に評価できる環境だ。

importValueによるモジュールの隔離実行

// spookycode.js
export function greeting() {
  return "Hello from the ShadowRealm!";
}

// メイン側
async function shadowGreeter() {
  const shadow = new ShadowRealm();
  const shadowGreet = await shadow.importValue(
    "./spookycode.js",
    "greeting"
  );
  shadowGreet(); // "Hello from the ShadowRealm!"
}
shadowGreeter();
モジュールソース(spookycode.js)
export function greeting() { ... }
ShadowRealmでimportValueして関数を受け取る
const fn = await shadow.importValue("./spookycode.js", "greeting");
外側のレルムで安全に実行
fn(); // "Hello from the ShadowRealm!"
※モジュール内部のグローバルは外側から隔離されており意図しない干渉は起きない。

importValue()はPromiseを返し第二引数で指定したエクスポート名の値だけを取り出す。モジュールの内部実装はShadowRealmの隔離環境で動くため外側のグローバル空間を一切汚さない。CSS-in-JSで使うテーマ計算モジュールなどをこの形で読み込めばグローバル変数を介したスタイルの競合を根本から断てる。

ShadowRealmが変えるCSS設計の安全性

ShadowRealmが変えるCSS設計の安全性
Before(グローバル共有)
テーマ変数: window.currentTheme = 'dark';
※広告スクリプトが window.currentTheme = 'light'; に上書き
After(ShadowRealmでテーマ管理を隔離)
テーマはShadowRealm内のモジュールで完結
※外部スクリプトからはアクセス不能。
※テーマ管理をShadowRealmに隔離することで予期せぬ上書きからCSS変数を守れる。

ShadowRealmはまだブラウザに実装されていないがCSS設計の安全性を高めるうえでいくつかの具体的な使い道が考えられる。

スタイルの衝突防止とテーマ分離

複数の独立したコンポーネントやマイクロフロントエンドがひとつのページに同居するケースではそれぞれが使うCSS変数やテーマオブジェクトが衝突しやすい。ShadowRealmで各コンポーネントのスタイル計算ロジックを包めばそれぞれのグローバル空間は完全に分離され変数の取り合いが起きない。

またユーザーごとにカスタマイズされたテーマを動的に生成するような仕組みでも計算ロジックをShadowRealmに閉じ込めれば他スクリプトによる意図しないテーマ書き換えを防げる。

テスト環境の清浄化

CSS-in-JSの単体テストではグローバルなスタイルシートの状態がテスト結果に影響を与えることがある。ShadowRealm上でテストを実行すればテストごとに完全にクリーンなグローバル環境が得られモックデータの準備も簡素化される。ブラウザテストとNode.jsテストの両方で同じ隔離機構を使える点もメリットだ。

実装状況と将来の展望

実装状況と将来の展望

ShadowRealmの提案はTC39のステージ2.7にある。これは「原則的に承認され検証段階にある」という位置づけでブラウザへの試験実装が始まれば仕様の微調整が入る可能性は残る。現時点では実際のブラウザやNode.jsで使うことはできない。

CSS-Tricksの記事でも指摘されているようにShadowRealmはセキュリティ境界ではなく完全性境界を提供するものだ。つまり悪意あるコードの実行を完全に防ぐサンドボックスではないがグローバル変数や組み込みオブジェクトを意図せず壊してしまうリスクを封じ込めるには十分な隔離性能を持つ。

ステージ3へ進み主要ブラウザが実装を始めればCSS-in-JSライブラリやテストランナー、サードパーティスクリプト管理の分野でいち早く活用が広がるだろう。

この記事のポイント

  • ShadowRealmはコードを独立したグローバル環境で実行するTC39提案のAPI
  • メインスレッド上で動くためスレッド間通信の複雑さがない
  • サードパーティスクリプトやCSS-in-JSのテーマ計算を隔離しスタイル競合を防げる
  • テストコードの実行環境としてもクリーンなグローバル空間を提供する
  • 現時点ではステージ2.7で未実装。ブラウザ対応はこれから