- はじめに — 6月25日のDeno 2.9、3週間のパッチ
- deno desktop — Electronの座に入ってきた実験
- ロックファイルシード — 今回のリリースでもっとも実務的な機能
- Node 26互換 — 「互換ターゲット」という言葉の正確な意味
- ベンダーベンチマークを正確に読む
- サプライチェーンのデフォルト — 24時間待機がデフォルトになった
- アップグレード時に引っかかるもの
- で、乗り換えるべきか
- おわりに
- 参考資料
はじめに — 6月25日のDeno 2.9、3週間のパッチ
去る5月、Deno 2時代を整理した記事でDenoの方向転換 — Nodeを拒んでいたランタイムがNode互換を第一級市民として受け入れたこと — を扱いました。今回はその続編です。Deno 2.9が2026年6月25日に出て、GitHubのリリースタグ基準でv2.9.0(6月25日)のあとv2.9.1(7月1日)、v2.9.2(7月8日)、v2.9.3(7月15日)まで毎週パッチが続きました。直前のマイナーである2.8.0が5月22日だったので、5週間隔のリリースリズムです。
リリースノートの見出しはdeno desktopですが、私の判断では実務者にとってより重要なのは別の2つの変化です。既存のnpm・pnpm・yarn・Bunのロックファイルをそのまま読み込む移行パス、そしてサプライチェーン防御機能がデフォルトでオンになったこと。一つずつ見ていき、ベンダーが出した性能数字は測定条件と抜けている点まで一緒に読みます。なお、TypeScriptコンパイラ側の激変はTS 7 GAとネイティブコンパイラの記事で別途扱いました — 本稿はランタイム層の話です。
deno desktop — Electronの座に入ってきた実験
deno desktopはWebスタックでネイティブデスクトップアプリを作る新しいサブコマンドです(PR #33441、6月16日マージ)。UIはwebviewで動き、ロジックはDenoで動き、成果物は単一の配布バイナリです。もっとも単純な形はエントリーポイントがUIをサーブすることですが、デスクトップエントリーポイント内のDeno.serve()はwebviewが開くポートに自動でバインドされるため、ポート配線は不要です。
// main.ts — これで全部です
Deno.serve(() =>
new Response(
"<!DOCTYPE html><h1>Hello from Deno desktop</h1>",
{ headers: { "content-type": "text/html" } },
)
);
deno desktop main.ts # ネイティブウィンドウが開きます
deno desktop # 現在のディレクトリのWebフレームワークを自動検出
deno desktop --hmr # 開発中のHMR
フレームワーク自動検出はdeno compileと同じ仕組みを使い、公式リリースノート基準でNext.js、Astro、Fresh、Remix、Nuxt、SvelteKit、SolidStart、TanStack Start、Vite SSRをサポートします。ランタイムにはDeno.BrowserWindow(ウィンドウ制御とwebview-Denoブリッジ)、Deno.Tray(システムトレイ)、macOS用のDeno.Dock、ネイティブダイアログとしてレンダリングされるprompt()/alert()/confirm()、バイナリパッチをバックグラウンドで適用するDeno.autoUpdate()が入りました。
レンダリングエンジンは--backendで選びます。デフォルトのwebviewはOS内蔵エンジン — WindowsのWebView2、macOS・LinuxのWebKit — を使うのでバイナリが小さく起動も速いですが、レンダリングがホストOSのエンジンバージョンに左右されます。--backend cefはChromium Embedded FrameworkでChromiumをまるごとバンドルし、全プラットフォームで同一のレンダリングを保証する代わり、リリースノートの表現で数十MBが追加され、ビルド時のダウンロードが必要です。まさにTauri(webview)対Electron(バンドルエンジン)のトレードオフを一つのツールの中で選ぶ構造です。
デプロイ側が興味深いところです。--outputの拡張子に従ってmacOSは.app・.dmg、Windowsは.exe・.msi、Linuxは.AppImage・.deb・.rpmとして出力され、MSIとdeb・rpmのインストーラーは純粋なRustで作られているため、プラットフォーム別のパッケージングツールチェーンなしにLinux CI一台から--all-targetsで全プラットフォームのバイナリを引き出せます。対応ターゲットはdeno compileと同じ5つ — Linux x64/arm64、Windows x64、macOS x64/arm64 — で、Windows arm64はまだありません。
いつ使うべきでないか。 ベンダー自身が2.9ではexperimentalであり、表面がまだ安定化中だと明言しています。プロダクションのデスクトップアプリを実験フラグの上に載せるのはそれ自体で答えが出ている判断であり、今日出すべきものなら、Electron・Tauriという実証済みの選択肢がそのままあります。webviewバックエンドを選べば「ユーザーのOSのWebKit・WebView2のバージョンごとにレンダリングが変わりうる」というTauriユーザーがすでに知っている痛みがついてきますし、cefを選べばElectron並みの容量が戻ってきます。この実験の本当の意味は今すぐのプロダクション採用ではなく、「ランタイム・コンパイラ・パッケージングがすでに一体化したツールがデスクトップまで飲み込もうとしている」という方向性のシグナルに近いものです。
ロックファイルシード — 今回のリリースでもっとも実務的な機能
移行コストの最大の塊はいつも「丁寧に固定してきた依存グラフを失うこと」です。2.9はここを直接狙います。deno.lockがないプロジェクトでdeno installを実行すると、package-lock.json、pnpm-lock.yaml、yarn.lock、bun.lockを読んで、まったく同じ解決バージョンと整合性ハッシュでdeno.lockをシードします(bun.lockサポートPR #35394)。再解決がないため、こっそり上がるバージョンもないというのが公式の説明です。シードは一方向です — 外部ロックファイルを読んでdeno.lockを作り、以降の管理はdeno.lockが担います。書き戻し(write-back)はしません。
$ deno install
Seeded deno.lock from package-lock.json
pnpmモノレポも対応しました。npm・yarn・Bunが使うpackage.jsonのworkspacesフィールドはもともと読んでいましたが、pnpmだけは別ファイルを使うところ、pnpm-workspace.yamlを見つけると既存のフィールドに触れずにpackages・catalog・catalogs設定を移してくれます(#34993)。そして本物のnodeバイナリがインストールされていない環境ではPATHにスタンドインのnodeを置いて自分自身に転送し、Node CLI引数を翻訳します(#34969) — Turbopackのワーカープールのようにnodeを直接呼ぶツールのための措置で、本物のnodeがあれば決して覆い隠さず、DENO_DISABLE_NODE_SHIM=1で切ることができます。
この一連の変更の意味は明快です。「Denoへの乗り換え」が依存グラフの再構築という賭けではなく、既存のロックファイルを維持したままコマンド2つで試してみて、駄目なら戻れる可逆的な実験になった、ということです。ただし、ロックファイル形式互換のエッジケース — pnpmが依存キーにpeerをインライン符号化するやり方のような — はリリース直前まで後続PRで調整され続けていたので、複雑なpnpmモノレポなら、シード結果のバージョンが元と一致しているか確認してから進むのが安全です。
Node 26互換 — 「互換ターゲット」という言葉の正確な意味
2.9はNode.js互換ターゲットをNode 26に引き上げました。process.versionがv26.3.0を報告し(#34747)、Denoが回しているnode-compatテストスイートも26.3.0に上がりました(#34746)。文脈を付け加えると — endoflife.date基準でNode 26は2026年5月5日に出たCurrent系列で、LTS昇格は2026年10月28日予定、実際の最新は7月8日の26.5.0です。つまり互換ターゲットは実際のNodeより2つパッチ分遅れています。
実質的な改善もいくつかあります。import "fs"のようなベア(bare)組み込みモジュール指定子がフラグなしで無条件にnode:fsに解決されるようになり、node_modules内のパッケージが組み込みモジュールを覆い隠していたバグもNodeと同じルール(組み込みが常に勝つ)で直りました。Node-APIはバージョン10を報告し、node:testにmock.module()・mock.timers・ファイルスナップショットが付き、process.resourceUsage()のような抜けていたAPIも埋まりました。
正直に指摘すべき部分です。「互換ターゲット26」はDenoが自分の回す互換テストスイートの基準バージョンを26.3.0に合わせたという意味であり、Node 26エコシステムが100%動くという保証ではありません。ネイティブアドオンやV8内部に依存するツール、Nodeの内部実装の細部に頼るパッケージは依然として個別確認の対象です。その境界がどこかはリリースノートが語ってくれないので、候補プロジェクトで実際にテストスイートを回してみる以外の近道はありません。
ベンダーベンチマークを正確に読む
リリースノートの性能数字です。すべてベンダー自己測定であり、条件はこう明記されています — 専用のx86_64 Linuxマシン1台、サーバーと負荷生成器を重ならないコアに固定、同時実行数100、oha 3回実行の中央値、コールドスタートはhyperfine 150回実行の平均、比較対象はDeno 2.8.0。
| 項目 | 2.8 | 2.9 | 変化 |
|---|---|---|---|
| コールドスタート(hello world) | 34.2 ms | 17.3 ms | 1.98倍速い |
| Deno.serve「realworld」(JSON POST + Bearerヘッダーのエコー) | 56.8k req/s | 72.4k req/s | 1.27倍 |
| Deno.serve plaintext | 77.0k req/s | 85.6k req/s | 1.11倍 |
| Deno.serve 1 MiBレスポンス | 1,617 req/s | 1,907 req/s | 1.18倍 |
| ピークRSS、realworld | 142 MB | 64 MB | 2.2倍減少 |
| ピークRSS、1 MiBレスポンス | 197 MB | 63 MB | 3.1倍減少 |
倍率は元の数値と算術的に合致します(自分で割って確認しました)。改善の出所もリリースノートが明かしています — スナップショットからnode:グローバルを遅延読み込みに切り出し、NodeブートストラップをNodeワーカーに限定し、遅延読み込みされるESMにV8コードキャッシュを付け、スナップショットを縮小したことがコールドスタートの半分を作り、HTTPスループットはDeno自前のHTTP/1.1サービングパス(#34446)が、RSSの平坦化とともにcrypto.subtleとconsoleのJSからRustへの移植が残りを作りました。
この数字が語っていないことも同じくらいはっきりしています。
- 比較対象が自分自身です。 2.8対2.9であって、NodeやBunとの比較ではありません。「1.27倍」は乗り換える理由ではなく、アップグレードする理由です。
- ループバックトラフィックです。 同じマシンでコアだけ分けて撃った負荷です。1 MiBワークロードの1,907 req/sは帯域として約16 Gbpsに相当しますが、これはNICを通らないループバックだからこそ出せる数値です。実際のネットワーク・TLS終端が挟まれば話は変わります。
- HTTP/1.1平文パスです。 新しいサービングパス自体がHTTP/1.1向けで、TLSやHTTP/2の数値は示されていません。
- スループットとRSSだけです。 レイテンシ分布 — p99のようなテールレイテンシ — はありません。3回の中央値なので分散の情報もありません。
- エコーワークロードです。 「realworld」という名前が付いていますが、DBもディスクもないJSONエコーです。実際のアプリのボトルネックはたいていランタイムの外側にあります。
その上で一つだけ選ぶなら、私はスループットではなくメモリを見ます。2.8ではRSSがワークロードによって約94 MBから197 MBまで膨らんでいましたが、2.9は測定されたワークロードにおいて62 MB前後で平坦に保たれます。同じマシンにより多くのインスタンスを載せられるという意味なので、コンテナ密度でコストを計算するチームにとっては、スループット1.1〜1.3倍よりこちらのほうが実質的な変化です — ただし、あくまで該当ワークロードでの結果という但し書きはそのまま残しておきます。
サプライチェーンのデフォルト — 24時間待機がデフォルトになった
2.9で静かに最大の政策変更はこれです。2.6で導入されたmin-release-age — 公開されてから一定時間が経っていないnpmパッケージバージョンのインストールを拒否する機能 — が24時間の窓でデフォルト有効化されました(#35458、リリース当日マージ)。根拠は単純です。npmサプライチェーン攻撃のかなりの割合は、悪意あるバージョンが公開されてから1〜2日以内に検知・公開取り消しされるので、待つだけで大きな部類の攻撃がふるい落とされます。先週のaxios事件のまとめで見たように、provenanceがあっても突破される穴がある以上、時間差は単純ながら実効性のある防御層です。
# .npmrc — 明示的な値がデフォルトより優先されます
min-release-age=72h # 3日に延ばす
min-release-age=0 # 完全にオフにする
運用でつまずきうる点を挙げておくと — 自社パッケージをnpmに公開してすぐにインストールするパイプラインは、デフォルト設定では24時間失敗するようになります。ホットフィックスを公開直後にデプロイに乗せるワークフローなら.npmrcで値を調整する必要があり、このトレードオフ(新しいパッチの遅延 vs 攻撃ウィンドウの遮断)は各自が判断する事柄です。npmパッケージに適用される機能だという点も明確にしておきます。
ここにオプトインの防御がもう一つ加わりました。trust-policy=no-downgradeはpnpmの設計に倣い、各バージョンの公開方式 — 2FAのライブ承認が付くstaged publishing、provenance証明が付くtrusted publishing、provenance単独 — の信頼順位を付け、以前のバージョンより弱い証拠で公開された新バージョンの解決をハードエラーにします。メンテナトークン窃取攻撃(リリースノートには2025年8月のs1ngularity事件が例として挙がっています)の典型的な痕跡である「provenanceできちんと公開されていたパッケージが突然普通のトークン公開で出てくる」状況を捕まえる仕組みです。レジストリ全体でのprovenance採用がまだ均一でないためデフォルトはオフだという説明まで、リリースノートが自ら限界を明かしている部分もそのまま移しておきます。
アップグレード時に引っかかるもの
動作変更が一つあります。Deno.serveの自動レスポンス圧縮がデフォルト無効に変わりました。既存バージョンの自動圧縮に頼って帯域を節約していたなら、2.9に上げた瞬間からレスポンスは非圧縮で出るようになります — サーバーごとにautomaticCompression: trueを指定するか、DENO_SERVE_AUTOMATIC_COMPRESSION=1環境変数でプロセス全体に復活させられます。CDNの後ろなら体感はないでしょうが、直接サービングしているならアップグレードチェックリストに載せておく項目です。
残りは実験ラベル付きで入ってきました。CSSモジュールインポート(with { type: "css" }でCSSStyleSheetを受け取るWeb標準の方法)は--unstable-raw-importsフラグの後ろにあり、deno compile --bundle — ツリーシェイキングしてから埋め込んでバイナリを小さくする — は実験フラグで、ベンダー自己測定でlodash hello worldが11.6 MBから1.5 MBに縮んだという数字が示されています(当然npm依存が重いプロジェクトほど効果が大きく、一般化は各自のビルドで確認すべきことです)。
テストランナーは目に見えて厚くなりました。t.assertSnapshot()の組み込みスナップショット、Deno.test.eachによるパラメータ化、CI分割の--shard、変更されたファイルの影響を受けるテストだけを選ぶ--changed、フレーキー対応の--retry、カバレッジ閾値まで — VitestやJestを呼んでいた理由を一つずつ消していく方向です。ほかにもdeno taskの入力ベースキャッシング、npm lsに相当するdeno list、そしてML-KEM・ML-DSA・SLH-DSAなどNISTポスト量子アルゴリズムのWeb Crypto実装(OpenSSH 10.4のML-DSAの話と同じ流れのランタイム版)も今回のリリースに含まれています。
で、乗り換えるべきか
整理してみます。
試す理由が生まれた場合
- 既存のNodeプロジェクトで、ロックファイルを失わずに可逆的に実験できるようになった — 移行コストの構造が変わったことが今回のリリースの核心です。
- フォーマッター・リンター・テストランナー・タスクランナー・コンパイラを一つに減らしたいチーム — 2.9のテストランナー強化で外部ツールへの依存が実際に減りました。
- npmサプライチェーン防御をツールのデフォルトとして受け取りたい組織 — 24時間待機のデフォルト化は現行の主要ランタイムの中でもっとも積極的なデフォルトです。
まだ早い場合
- ネイティブアドオンやNode内部APIに深く依存したコードベース — 「互換ターゲット26」が保証しない領域です。
- プロダクションのデスクトップアプリ — deno desktopは実験であり、Electron・Tauriを置き換える段階ではありません。
- Bun特化API(
bun:sqlite、Bun.serveなど)の上に立つコード — ロックファイルは読んでくれますが、APIは翻訳してくれません。 - すでにNode 26で問題なく動いていて、移行で得られる具体的な項目を挙げられないチーム — ベンチマーク表が示すとおり、2.9の数字はNode対比の優位性を主張するものではありません。
参考までに、競合ランタイム側の状況を一言残すと — BunのGitHubリリース基準で最後の安定版は5月13日のv1.3.14で、2か月新しい安定リリースが出ていませんが、リポジトリのコミットは今日も続いており、停滞と読むのは無理があります。ただし、毎週パッチを出すDenoとのリリースリズムの差は記録しておくに値する事実です。Bun側の状態評価は5月のBun回顧を参照してください。
おわりに
Deno 2.9を貫く論理は「移行コストを構造的に下げること」です。ロックファイルをそのまま読み、nodeバイナリの場所を埋め、pnpmワークスペース設定を移し、Node 26互換ターゲットに追随すること — すべて同じ方向です。2年前にNode互換を受け入れた自己否定が、今や競合のロックファイル形式まで読む段階に来ました。その上に載るdeno desktopはまだ実験ですが、ツール統合という同じ論理の延長であり、min-release-ageのデフォルト化は、ランタイムがサプライチェーン防御のデフォルトを決める時代が来たというシグナルです。
ベンダーの数字は条件とともに読むと有用でした — ループバックのマイクロベンチマークという限界の中で、コールドスタート半減とRSSの平坦化はメカニズムの説明まで付いた信頼できる改善です。ただし、その表にNodeとBunがいないという事実こそがこのリリースの性格を正確に語っています。2.9は「自分たちのほうが速い」ではなく「乗り換えるコストを下げた」というリリースであり、その主張だけは数字ではなく機能一覧が証明しています。
参考資料
- Deno 2.9 公式リリースノート
- denoland/deno GitHubリリース(v2.9.0~v2.9.3の日付確認)
- PR #33441 — feat: deno desktop subcommand
- PR #35458 — feat: enable default minimum dependency age
- PR #34746 — node-compatテストスイート26.3.0への引き上げ · PR #34747 — process.version v26.3.0
- PR #34446 — Deno自前のHTTP/1.1サービングパス
- Node.jsリリーススケジュール — endoflife.date
- Deno 2時代の整理 — Node互換の時代(関連記事)
- axios npm侵害事件 — provenanceが見逃したもの(関連記事)
현재 단락 (1/72)
去る5月、[Deno 2時代を整理した記事](/blog/culture/2026-05-14-deno-2-3-node-compatibility-era-runtime-jsr-fresh-de...