WinterTCは、Node.js・Deno・Bun・Cloudflare WorkersといったサーバーサイドのJavaScriptランタイムに共通のAPIを決める、Ecma Internationalの技術委員会TC55の通称です。W3CのコミュニティグループだったWinterCGが標準を発行できる場所を求めて移った先で、2025年12月に最初の標準ECMA-429が公開されました。本記事では、規格の中身に加えて、ECMA-429が求める74項目を3つのランタイムで実際に数えた結果と、2026年10月にDenoの開発終了が発表されたあとの状況までを整理します。
まとめ:WinterTCの現在地(2026年11月時点)
- WinterTCの正式名はEcma TC55「Web-interoperable server runtimes」。言語仕様を決めるTC39と同じEcmaの技術委員会で、扱うのは言語の外側にある実行環境のAPI。
- 移管の向きはWinterCG(W3C)からWinterTC(Ecma)。W3C側のWinterCGは2025年4月3日に閉鎖済み。
- 標準はECMA-429「Minimum common web API」第1版(2025年12月)。ランタイム識別子のRuntime Keysは2026年6月にEcma TR/114第1版として発行された。
- ECMA-429が求める74項目の存在を実測で確かめると、Deno 2.9.8は74、Bun 1.4.4は73、Node.js 26.13.0は71だった。Node.jsに無い
reportError()とselfは、規格のNode.js向け例外の対象外。 - 2026年10月9日、Denoチームの Cloudflare 合流が発表され、Denoランタイムの開発は1年後に終了する。Web標準APIで書いた部分は移行先で再利用しやすい(実装差の確認は要る)が、
Deno.*は書き換えが要る。 - WinterJSはWinterCGが公認していない独立したランタイムで、2026年3月にアーカイブされた。
WinterTCの役割:Ecma TC55が標準化する範囲
EcmaのTC55のページは、委員会の目的を「複数のECMAScript実行環境、とりわけWebサーバーの相互運用性を高める『最小共通API』と、その適合性の検証可能な定義を標準化すること」と定めています。既存のWeb標準に無いサーバー固有の機能、たとえばコマンドライン引数や環境変数の扱いについて、新しいAPIを標準化することも認められています。Array.prototype の挙動はTC39、fetch() がサーバー上に存在するかどうかはTC55、という分担です。
2026年に入って、同じページの作業計画に「適合レベル(conformance levels)」の項目が加わりました。最小共通APIを第1の適合レベルとし、それを超えるサーバー向けのレベルを今後定める、という構想です。ECMA-429は「全ランタイムが共有する最低ライン」で、ファイルやソケットのようなサーバーらしい機能は別のレベルで扱う方向が明文化されたことになります。
体制も変わりました。議長を務めていたIgaliaのAndreu Botella氏が2026年7月以降は務められないとして交代し(2026年6月26日の議事録)、11月時点の議長団はEthan Arrowood氏とChris de Almeida氏(いずれもOpenJS Foundation)、James Snell氏(Cloudflare)の3人です。幹事はEcma InternationalのAki Rose Braun氏とSamina Husain氏が続けています。2026年7月10日の会合では、ECMA-429第1版の発行を受けて、隔週だった会合を毎月最終金曜の月1回に移すことが決まりました。
WinterCGからWinterTCへ移った経緯
Community Groupでは標準を発行できない制約
WinterCG(Web-interoperable Runtimes Community Group)は、wintertc.orgのFAQによれば2022年5月にW3Cのコミュニティグループとして発足しました。ただしW3Cのコミュニティグループには、勧告や標準を正式に発行する権限がありません。そこで2024年12月にEcmaへの移管が決まり、2025年1月10日にW3CとEcmaが告知しました。
移管の向きはWinterCGからWinterTCで、逆ではありません。WinterCGがEcmaへ移り、そこでの名前がWinterTCです。
2025年4月3日のWinterCG閉鎖と現在の参照先
W3CのWinterCGページには「closed on 2025-04-03」と明記されています。wintercg.org は wintertc.org へ転送され、GitHub組織も wintercg から WinterTC55 へ移りました。リンクを貼り直すなら wintertc.org と github.com/WinterTC55 です。
なお、2026年7月31日の議事録には、配信に使っていた「Deno Deploy」の提供終了(sunset)によってTC55のWebサイトが停止していると記録され、同じ会合でCloudflareへの移設が承認されています(10月のDeno合流発表より前の話です)。11月3日時点で wintertc.org と仕様のプレビューページは表示できます。
ECMA-429の必須API:74項目の内訳
ECMA-429は自らを「Minimum common web APIの2025年スナップショット」と位置づけ、W3CとWHATWGのWeb標準から選んだAPIを、ブラウザとサーバーに共通する最低限の能力として定義しています。仕様の原文には年次での刊行を目指すとありますが、Ecmaの公式一覧に載っているのは11月3日時点で第1版だけです。
規格の列挙を本記事で数えると、ランタイムが globalThis に出すべき項目は次の74個になります(globalThis 自体と、エラー系のイベントハンドラ3つは別に扱いました)。
| 区分 | 項目数 | 主な項目 |
|---|---|---|
| インターフェース | 43 | Request・ReadableStream・URLPattern など |
| WebAssembly | 16 | Module・Memory・compile() など |
| メソッド・プロパティ | 14 | fetch()・reportError()・self など |
| navigator.userAgent | 1 | ランタイムを表す文字列 |
| (別枠)イベントハンドラ | 3 | onerror・onunhandledrejection・onrejectionhandled |
インターフェースは13の仕様にまたがります。DOM(AbortController・Event・EventTarget など)、HTML(MessageChannel・ErrorEvent など)、Fetch、File API、Compression、Streams、Encoding、URL、URL Pattern、Web Cryptography、High Resolution Time などです。ブラウザで書き慣れたAPIのうち、どこまでがサーバー側でも保証されるのかが規格として固定されました。WebAssembly関連が丸ごと必須に入っているので、WebAssemblyとJavaScriptの連携をエッジで組むときも前提にできます。
第1版が対象外にした領域
ファイルシステム、TCP接続、プロセス制御、子プロセスの起動は第1版に含まれていません。Web Workersも必須ではなく、「WorkerGlobalScopeに相当するスコープを持つなら、そこにもエラー系ハンドラを出すこと」という条件付きの規定です。既存アプリのグローバル変数と衝突しないよう、規格が追加を求めるプロパティは、Web標準でreadonlyとされていても上書き・削除を許してよいとも定めています。ECMA-429に準拠していても、ランタイム間でコードがそのまま動くとは限りません。保証されるのは最低ラインだけです。
Node.jsを念頭に置いた例外規定
ECMA-429は原則として、グローバルオブジェクトがEventTargetで、そこで error・unhandledrejection・rejectionhandled の各イベントが発火することを求めています。しかしNode.jsのグローバルはEventTargetではありません。そこで規格は、歴史的な事情でそれができないランタイムには、同じ情報を渡せる別の仕組みでの通知を認めました。その場合、onerror などのグローバルプロパティは「持ってはならない(shall not support)」とされ、ErrorEvent と PromiseRejectionEvent の実装は「求めない(not required)」とされています。前者は禁止、後者は任意で、強さが違います。Node.jsが process で uncaughtException や unhandledRejection を発火させる今の挙動は、この例外の範囲に収まります。
Node.js 26・Deno 2.9・Bun 1.4で必須APIを実測した結果
規格に「準拠」と書かれていても、実際にどこまで揃っているかはランタイム次第です。上の74項目と別枠の3ハンドラが存在するかを、2026年11月3日時点の最新版でそれぞれ確かめました(macOS x86_64、Node.jsはLTSの26.13.0)。判定は「globalThis にその名前の関数またはオブジェクトがあるか」で、中身の挙動までは見ていません。
| ランタイム | 74項目中の存在 | 無かった項目 | グローバルがEventTarget | navigator.userAgent |
|---|---|---|---|---|
| Deno 2.9.8 | 74 | なし | はい | Deno/2.9.8 |
| Bun 1.4.4 | 73 | PromiseRejectionEvent | いいえ | Bun/1.4.4 |
| Node.js 26.13.0 | 71 | PromiseRejectionEvent・reportError・self | いいえ | Node.js/26 |
確認に使ったスクリプトの要点は次のとおりです。下は抜粋で、実際には上の表の43個と14個をすべて配列に並べ、WebAssemblyの16項目と navigator.userAgent は別に調べています。同じファイルを3つのランタイムでそのまま実行できます。
const ifaces = ["AbortController", "ReadableStream", "URLPattern", "PromiseRejectionEvent" /* …43個 */];
const props = ["fetch", "structuredClone", "reportError", "self" /* …14個 */];
const missing = [...ifaces, ...props].filter((n) => globalThis[n] === undefined);
console.log(globalThis.navigator?.userAgent, missing);
// node check429.mjs / deno run check429.mjs / bun check429.mjs
Node.jsに無かった3項目のうち、PromiseRejectionEvent は例外規定で任意とされた側です。一方 reportError() と self は例外の対象ではないので、Node.js 26は厳密にはECMA-429を満たしていません。ライブラリで self をグローバルの参照に使うと、Node.jsだけ ReferenceError になります。グローバルを指すなら globalThis を使うのが確実です。
別枠の3ハンドラでは、Node.jsは3つとも持たず、例外規定どおりでした。Denoは onerror と onunhandledrejection を持つ一方、onrejectionhandled は無し。Bunはグローバルが EventTarget のインスタンスではないのに addEventListener と onerror を持つ中間的な実装で、規格の原則にも例外規定にもきれいには当てはまりません。
未処理のPromise拒否:ランタイム別の通知経路
未処理のPromise拒否をどこで受けられるかも測りました。Denoは addEventListener("unhandledrejection") で受けられ、イベントの型は PromiseRejectionEvent でした。Bunは globalThis.addEventListener が関数として存在するのに、このイベントは発火しませんでした。一方 process.on("unhandledRejection") だけを登録した場合は、Node.jsと、Node.js互換層を持つDeno・Bunの3つとも呼ばれています。ただしDenoで両方を登録して preventDefault() を呼ぶと、process.on 側は呼ばれませんでした。
この結果から、Node.js・Deno・Bunを対象にするなら、process があればそちらを使い、無いときだけ addEventListener に落とす書き方が確実です。Cloudflare Workersは Node.js互換の設定で process を持つため、今回の実測の対象外で、通知経路は別に確かめる必要があります。
export function onUnhandledRejection(handler) {
if (typeof globalThis.process?.on === "function") {
// Node.js・Bun・Deno(Node.js互換層)で発火を確認
globalThis.process.on("unhandledRejection", (reason) => handler(reason));
} else if (typeof globalThis.addEventListener === "function") {
// process を持たず、グローバルが EventTarget のランタイム向け(未実測)
globalThis.addEventListener("unhandledrejection", (e) => handler(e.reason));
}
}
Runtime Keys(ECMA TR/114)と実行時のランタイム判定
Runtime Keysの用途:設定ファイルとランタイムの識別
Runtime Keysは、2026年6月にEcma TR/114第1版として発行された技術報告です。用途は「プロジェクト設定ファイル、パッケージマニフェスト、条件付きエクスポート、ランタイム検出の仕組みなどでECMAScriptランタイムを識別するための一貫した仕組み」と説明され、ブラウザでの利用は想定していません(ブラウザはbrowserslistなど別の仕組みで扱うよう案内しています)。実体の runtime-keys.json は版1.0.0・最終更新2026年1月20日で、登録は23件です。
- 汎用ランタイム:node・deno・bun・kiesel・andromeda・rhino
- エッジ・サーバーレス:workerd・edge-light・edge-routine・fastly・netlify・azion・arvancloud・lagon・convex・wasmer
- アプリ・組込み:electron・react-native・moddable・quickjs・quickjs-ng・pythonmonkey
- その他:react-server(React Server Componentsの実行条件)
区分は本記事による整理で、レジストリ側に区分はありません。11月3日時点では、AWSの軽量ランタイムLLRTを llrt として追加するプルリクエストと、deno を非推奨(deprecated)にするプルリクエストが審査待ちです。レポートは掲載されたランタイムがECMA-429に準拠していることを意味しないと明記しているので、「Runtime Keysに載っている=WinterTC準拠」と読むのは誤りです。
実行中のランタイムをコードから知る手段は、Runtime Keysではなく、ECMA-429が必須にした navigator.userAgent です。規格は版番号を含まない単一のトークンにすることを推奨していますが、推奨にとどまります。実測でも Node.js/26、Deno/2.9.8、Bun/1.4.4 と、3つとも版番号が付きました。Node.jsの公式ドキュメントも「ランタイム名とメジャーバージョン番号」を返すと書いています。実測したこの形式では完全一致の比較が外れるので、/ より前を切り出します。
const ua = globalThis.navigator?.userAgent ?? "";
const runtime = ua.split("/")[0]; // "Node.js/26" なら "Node.js"
if (runtime === "Cloudflare-Workers") {
// KVやDurable Objectsなど固有APIを使う分岐
} else if (runtime === "Node.js" || runtime === "Bun") {
// process や node:fs を使う分岐
}
規格はこの値を「不透明で構造を持たない値」として扱うよう求めています。特定のAPIが要るだけなら、typeof CompressionStream !== "undefined" のような機能検出のほうが壊れにくく、規格の意図にも沿います。userAgent での分岐は、ログへの記録や、機能検出では区別できない実装差を避けたいときに限るのが安全です。
標準化が進んでいるテーマ(2026年11月時点)
Iterable Streams:Node.js 26に実験実装
Web Streams APIを非同期イテレーション前提で作り直す提案がWinterTC55/iter-streamsです。ストリームを独自クラスではなく AsyncIterable<Uint8Array[]> として表し、変換は呼び出し側が読みにいったときだけ実行します。READMEは今も「本番で使わないこと」と明記しています。
この草案はNode.jsの node:stream/iterとして v25.9.0 から実装されています。安定度は「Stability: 1(Experimental)」で、起動時に --experimental-stream-iter を付けないと読み込めません。手元のNode.js 26.13.0では、フラグ無しだと ERR_UNKNOWN_BUILTIN_MODULE で止まり、フラグを付けると実験的機能の警告を出したうえで動きました。Deno 2.9.8とBun 1.4.4には、このモジュール自体がありません。
// node --experimental-stream-iter iter.mjs
import { from, pull, text } from "node:stream/iter";
import { compressGzip, decompressGzip } from "node:zlib/iter";
const compressed = pull(from("Hello, WinterTC!"), compressGzip());
console.log(await text(pull(compressed, decompressGzip()))); // Hello, WinterTC!
2026年7月31日の議事録によると、提案者のJames Snell氏はNode.jsでの実装経験を仕様に反映させるため、仕様の変更はしばらく見送る方針です。WHATWGのStreams標準へ知見を還元し、この提案自体はアーカイブされる可能性もあると記録されています。今の段階で業務コードに入れる対象ではありません。
Fetch:部分集合の規格から「Fetch posture」の提案へ
当初はFetch標準のうちサーバーで実装すべき部分集合を決める計画でしたが、部分集合では足りないとして方針が変わりました。いま議論されているのは、ブラウザ向けの制約をサーバー環境ではどう扱うかをFetch標準そのものに書き込む「Fetch posture」の提案(2026年7月)です。提案は、各ランタイムが禁止ヘッダーやCORSを独自に緩めており、その差が文書化されていないことを問題に挙げています。
実際、Fetch標準でスクリプトから設定できない Cookie・Host・Origin を new Request() に渡すと、Node.js・Deno・Bunの3つとも値をそのまま保持しました。ECMA-429も注記で、サーバーにはoriginの概念が無いため、ネットワーク要求にOriginヘッダーを付けるFetch標準の要件に違反せざるを得ないと例示しています。ただしFetch標準そのものには、ブラウザ以外での扱いがまだ書かれていません。並行して、interim responses・構造化ヘッダー・トレーラーをFetch標準へ追加するプルリクエストもWHATWGに出されています。
Sockets APIとHTTPサーバーAPIの提案状況
ブラウザ以外の環境でTCP接続を張るSockets APIは、Cloudflare Workersの connect() が実装例として挙がっています。7月31日の議事録には、James Snell氏が2026年末までにNode.jsへ実装することを目標にしていると記録されています。HTTPサーバーAPIのリポジトリは2026年2月6日に作られたまま空です。
適合性の判定については、TC55はWeb Platform Testsの一部を指標に使う方針で、2025年にどのテストを含めるかの選別を進めていました。ただし11月3日時点で、その結果を公式の適合性テストとして公開したものは見つかりませんでした。
DenoのCloudflare合流がWinterTCに与える影響
2026年10月9日、DenoはDenoチーム全員がCloudflareに合流すると発表しました。中身は次のとおりです。
- Denoランタイムは1年間、バグ修正とセキュリティ修正の月次リリースを続け、その後開発を終える(オープンソースとしては残る)
- Deno Deployは6か月運用したのち終了し、有料ユーザーにはCloudflare Workersへの移行を支援する
- JSRは運用を続け、基盤をCloudflareへ移す
WinterTCにとっては、今回の存在確認で74項目が全部揃っていた唯一のランタイムが先細りすることになります。2026年6月26日の議事録にも、主要ランタイムのほとんどがTC55に参加しておらず、例外はDenoだという指摘が残っています。Runtime Keysで deno を非推奨にする提案が出ているのも、この発表を受けたものです。
移行する側にとっては、ECMA-429の範囲で書いたコードを再利用しやすい点が効いてきます。fetch()・Request・Response・Web Crypto・Streamsで書いたハンドラは、Cloudflare WorkersやBunでも書き換えが少なく済みます。ただし実装差はあるので、移行先での動作確認は必要です。書き換えが必要になるのは Deno.serve() や Deno.openKv() のような Deno.* 名前空間の部分です。具体的な移し方はDeno Deploy終了と移行手順、移行先の選び方はDeno Bun 比較で扱っています。
WinterJSとの混同に注意:2026年3月にアーカイブされた非公認ランタイム
WinterCGやWinterTCを調べると、WinterJSという名前がよく出てきます。WasmerがService Workerスクリプトを実行するために作ったJavaScriptサーバーですが、WinterJSのREADME自身が「名前にWinterを含むが、WinterCGから公式に承認されたものではない」と明記しています。
リポジトリは2026年3月17日の更新で「Edge.jsを優先して非推奨」と告知し、そのままアーカイブされました。WinterCGの公式な成果物ではなく、すでに開発も止まっているので、新しく採用を検討する対象ではありません。
WinterTC準拠を判断基準にすべきでない場面
誤解されやすいのは、既存のNode.jsアプリを別ランタイムへ移す場面です。node:fs や node:child_process はECMA-429の対象外なので、移行先が準拠していても移せるかどうかの判断材料になりません。見るべきは移行先のNode.js互換層の範囲で、BunやCloudflare Workersはそれぞれ独自に互換範囲を公開しています。
デプロイ先が1つに決まっている場合も同じで、Cloudflare Workersでしか動かさないなら、KVやDurable Objectsなどの固有APIを素直に使ったほうが速く書けます。性能や費用で選ぶ場面でも関係ありません。ECMA-429は共通APIと実装上の要件を定める規格で、実行速度・メモリ・コールドスタート・課金体系には触れないからです。
WinterTCが効くのは、ライブラリの作者がランタイムを問わず動くコードを配りたいときと、エッジとオリジンで同じロジックを共有したいとき、そしてDenoのように採用中のランタイムの先行きが変わったときです。特定のランタイムに縛られない構成で業務システムを組みたい場合は、フルスクラッチ開発の段階からWeb標準APIを境界にした設計を検討できます。
よくある質問
WinterTCとWinterCGは同じものですか?
同じ取り組みの前身と後継で、運営団体が違います。W3CのコミュニティグループだったWinterCGが2024年12月にEcma Internationalへの移管を決め、TC55として再出発したときの通称がWinterTCです。WinterCGは2025年4月3日に閉鎖されました。
ECMA-429はいつ公開され、次の版はいつですか?
第1版が2025年12月に公開されました。内容は2025年時点のスナップショットで、委員会は年次での刊行を目指すとしています。ただし2026年11月3日時点で、Ecmaの公式一覧にある版は第1版だけです。
どのランタイムがWinterTCに対応していますか?
公式の適合性テストとして公開されたものは、2026年11月3日時点で見つかりませんでした。本記事で74項目の存在を確かめた範囲では、Deno 2.9.8が74、Bun 1.4.4が73、Node.js 26.13.0が71でした。Runtime Keysの23件に載っていることは、準拠の保証ではありません。
Denoの開発終了でWinterTCはどうなりますか?
WinterTCはEcmaの委員会で、Denoとは別に存続します。議長団はOpenJS FoundationとCloudflareの3人です。Iterable StreamsはNode.jsに実験実装があり、Sockets APIもNode.jsへの実装が目標に掲げられています。Deno向けに書いたコードのうち、Web標準APIの部分は他のランタイムでも再利用しやすいものの、移行先での動作確認が必要です。
WinterJSはWinterTCの公式ランタイムですか?
違います。WasmerのWinterJSはWinterCG仕様への対応を掲げていた独立したランタイムですが、README自身がWinterCG非公認だと述べています。2026年3月にEdge.jsを理由に非推奨となり、リポジトリもアーカイブされました。