Deno 2.9 で登場したデスクトップ機能 (Deno Desktop) - Electron / Tauri との違いを実感

| 12 min read
Author: masahiro-kondo masahiro-kondoの画像

はじめに

#

Deno 2.9 リリースおめでとうございます。

Electron 大好きな自分としても気になるのはやはり Deno Desktop です。
Tauri と同様 WebView をバックエンドにする構成と Electron と同様 Chromium ベースの構成を選べるとのことで、これは試すしかないと思いました。

公式ドキュメントは以下にあります。

Caution

Deno ブログには以下のように書かれており、2.9 時点ではデスクトップ機能は実験的段階です。

deno desktop is experimental in 2.9. The surface described here is stabilizing and some platform features are still landing.

使ってみる

#

まずは 2.9 にアップグレードしておきます[1]

deno upgrade

main.ts に Deno.serve を使って普通にサーバープログラムを書きます。

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 main.ts

⚠ deno desktop is experimental and subject to change
Check main.ts
Compile main.ts to hello.dylib

Embedded Files

hello.dylib
└── main.ts (430B)

Files: 1.91KB
Metadata: 1.38KB
Remote modules: 12B

Downloading laufey webview backend for aarch64-apple-darwin (v0.4.0)
Download laufey-webview-aarch64-apple-darwin.tar.gz 97.44KiB/97.44KiB
Codesigning bundle with identity "-"
hello.app/Contents/MacOS/laufey_webview: replacing existing signature
hello.app/Contents/MacOS/hello.dylib: replacing existing signature
Bundle hello.app

最後の出力で、ルートに hello.app (macOS のアプリ実行ファイル)が生成されており、起動できます(Windows の場合は、hello.exe が生成されます)。

Hello from Deno Desktop

Deno のコンセプト通り、追加のモジュールや設定なし(Out of the box)でデスクトップアプリが生成されました。

Deno Desktop の開発体験

#

HMR (Hot Module Replacement) オプション付きで起動することで、ローカルの開発サーバを立ち上げ、コード変更を検知してアプリ内容を即時更新してくれます。

deno desktop --hmr main.ts
⚠ deno desktop is experimental and subject to change
Compile main.ts to file:///Users/kondoumh/Library/Caches/deno/desktop/5f4a00908e99d886/hello.dylib

Embedded Files

hello.dylib
└── main.ts (422B)

Files: 1.9KB
Metadata: 1.38KB
Remote modules: 12B

Running desktop app with HMR (watching /Users/kondoumh/dev/deno-study/desktop/hello)
Runtime loaded successfully from: /Users/kondoumh/Library/Caches/deno/desktop/5f4a00908e99d886/hello.dylib
Runtime started
[desktop] dylib path: "/Users/kondoumh/Library/Caches/deno/desktop/5f4a00908e99d886/hello.dylib"
Listening on http://127.0.0.1:52958/

main.ts のコードを書き換えると、保存後すぐに画面へ反映されます。

Information

Electron では HMR は標準では利用できず、別途 Forge などで開発サーバーを起動する必要があります。

UI の ローカル HTTP サービスによる実現

#

Electron (Forge など) ではローカルサーバーの利用は開発時が中心で、配布後は file:// でアセットを読む構成が一般的です。

これに対し Deno Desktop は、配布後のバイナリでもローカル HTTP サーバーを内部起動し、空きポートを自動割り当てして UI を描画します。サーバーはプロセス内で閉じており、外部公開はされません。ポート衝突を意識せずに済むのも良い点です。

この「開発時もビルド済みバイナリでも、同じ HTTP 実行モデルで UI を提供する」設計により、

  • 開発時とデプロイ時の挙動に差がない
  • コンテンツはブラウザとデスクトップで同じ動きをする
  • Next.js などのフレームワークがそのままデスクトップアプリの中で動く

といったメリットが得られます。

DevTools の起動

#

Electron や Tauri と同様、DevTools によるデバッグが可能です。BrowserWindow を起動し、openDevtools メソッドを呼ぶだけです。

const win = new Deno.BrowserWindow({ 
  title: "My Deno Desktop App", 
  width: 800, 
  height: 600,
});

win.openDevtools();

Information

いまのところ、DevTools のフルサポートはバックエンドを cef にしている時のみです。

以下のように、指定して起動する必要があります。

deno desktop --hmr --backend=cef main.ts

open devtools

バックエンドとフロントエンドの通信(Bindings)

#

Electron の IPC 通信は render.js と main.js を preload.js 経由でブリッジする必要があり、かなり面倒です。Deno デスクトップでは BrowserWindow にバインドした関数を bindings というグローバルオブジェクトにより簡単に呼び出すことができます。
Deno ランタイムとレンダリングバックエンドはスレッドやプロセスとして動作し、呼び出しはプロセス内チャネルを介して行われます。このサンプル構成ではソケットベースの IPC を直接扱わずに済むため、Electron の ipcMain / ipcRenderer、Tauri の invoke と比べて見通しよく書けるのが利点です。

実際のコードで見てみましょう。

const win = new Deno.BrowserWindow({ 
  title: "Bindingsのテスト", 
  width: 800, 
  height: 600,
});

// ==========================================
// 1. バックエンド側:フロントから呼ばれる関数を登録
// ==========================================
win.bind("getSystemInfo", async (userName) => {
  console.log(`[Deno側] フロントエンドから呼ばれました! 引数: ${userName}`);
  
  // Denoの機能を使ってOSの情報を取得
  const denoVersion = Deno.version.deno;
  const os = Deno.build.os;

  // 少し重い処理をシミュレート(0.5秒待つ)
  await new Promise(resolve => setTimeout(resolve, 500));

  // フロントエンドに返すデータ(JSON化できるものなら何でもOK)
  return {
    message: `こんにちは、${userName}さん!`,
    os: os,
    denoVersion: denoVersion
  };
});

// ==========================================
// 2. フロントエンド側:画面のHTMLを返す
// ==========================================
Deno.serve(() => {
  const html = `
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8">
      <title>Bindings Test</title>
    </head>
    <body>
      <h1>Deno Desktop Bindings</h1>
      <button id="btn">システム情報を取得</button>
      <pre id="result">ここに結果が出ます</pre>

      <script>
        // ボタンが押された時の処理
        document.querySelector('#btn').addEventListener('click', async () => {
          const resultArea = document.getElementById('result');
          resultArea.textContent = "取得中...";

          try {
            // 💡 bindings を使ってバックエンドの関数を呼び出す
            const data = await bindings.getSystemInfo("kondoumh");
            
            // 結果を画面に表示
            resultArea.textContent = JSON.stringify(data, null, 2);
          } catch (error) {
            resultArea.textContent = "error: " + error.message;
          }
        });
      </script>
    </body>
    </html>
  `;

  return new Response(html, {
    headers: { "content-type": "text/html" },
  });
});

アプリ画面です。システム情報を取得 ボタンをクリックするとしばらく呼び出し中になり、結果が表示されます。

Bindings before

結果が表示された状態。

Bindings after

アプリを起動しているバックエンドでは次のようにログが出ています。

[Deno側] フロントエンドから呼ばれました! 引数: kondoumh

すごくシンプルです。OS のネイティブ機能と Web UI を簡単に連携できるのがいいですね。

メニュー の利用

#

アプリケーションメニューの実装。

  • BrowserWindow の setApplicationMenu メソッド内でメニューオブジェクトを定義して渡します。
  • BrowserWindow にイベントリスナーを登録してメニューがクリックされた時の振る舞いを実装します。

role などは Electron と同じですね。

win.setApplicationMenu([
  {
    submenu: {
      label: "File",
      items: [
        {
          item: {
            label: "New",
            id: "new",
            accelerator: "CmdOrCtrl+N",
            enabled: true,
          },
        },
        {
          item: {
            label: "Open…",
            id: "open",
            accelerator: "CmdOrCtrl+O",
            enabled: true,
          },
        },
        "separator",
        {
          item: {
            label: "Save",
            id: "save",
            accelerator: "CmdOrCtrl+S",
            enabled: true,
          },
        },
        { role: { role: "quit" } },
      ],
    },
  },
  {
    submenu: {
      label: "Edit",
      items: [
        { role: { role: "undo" } },
        { role: { role: "redo" } },
        "separator",
        { role: { role: "cut" } },
        { role: { role: "copy" } },
        { role: { role: "paste" } },
      ],
    },
  },
]);

win.addEventListener("menuclick", (e) => {
  const detail = (e as CustomEvent).detail;
  switch (detail.id) {
    case "new":
      console.log("New clicked");
      break;
    case "open":
      console.log("Open clicked");
      break;
    case "save":
      console.log("Save clicked");
      break;
  }
});

コンテキストメニューの実装。
Deno.MenuItem の配列を作成して、BrowserWindow の showContextMenu に座標とともに渡します。

const contextMenu: Deno.MenuItem[] = [
  { item: { label: "Copy", id: "copy", enabled: true } },
  { item: { label: "Paste", id: "paste", enabled: true } },
  "separator",
  { item: { label: "Properties…", id: "props", enabled: true } },
];

// Trigger from a right-click. The webview may not forward the browser
// `contextmenu` event, so handle the secondary mouse button on the window.
win.addEventListener("mousedown", (e) => {
  if (e.button === 2) {
    win.showContextMenu(e.clientX, e.clientY, contextMenu);
  }
});

win.addEventListener("contextmenuclick", (e) => {
  if (e.detail.id === "copy") { console.log("Copy clicked"); }
  if (e.detail.id === "paste") { console.log("Paste clicked"); }
  if (e.detail.id === "props") { console.log("Properties clicked"); }
});
Information

ここではメニューのクリックイベントでログを出力していますが、ログ自体はアプリを起動しているターミナル側に出ますのでご注意ください。

フレームワークを利用した開発

#

Deno.serve() を利用したサンプルを見てきましたが、Deno デスクトップでは、以下のフレームワークとともに利用可能です。これらのプロジェクトのディレクトリで deno desktop を起動すると、フレームワークを自動検出してアプリを構成します。多くのモダンフレームワークがサポートされています。

  • Next.js
  • Astro
  • Fresh
  • Remix
  • Nuxt
  • SvelteKit
  • SolidStart
  • TanStack Start
  • Vite

ローカルで動いてるのに SSR を使うというのがなんとも不思議な感じですが、ちゃんと動いてセキュアであればヨシ!という感じでしょうか。

Next.js のアプリを作成します。

deno run -A npm:create-next-app@latest

作成したプロジェクトディレクトリへ移動して実行します。

cd <project-dir>
deno desktop -A

Next desktop app

Next.js のアプリが、外部サーバーなしでまるっとデスクトップ内で動いてるのは不思議な感じです。

バックエンドの選択について

#

デスクトップアプリは配布するバイナリのサイズも重要です。小さいに越したことはありません。

Electron は Chromium を内包するため、インストールされたバイナリサイズは300MBぐらいの大きさになったりします。

Deno Desktop の場合、OS にプリインストールされている WebView を使えば70MB程度です。CEF(Chromium) だとやはり300MB程度になります。

OS 依存の WebView だと、Windows と Mac で微妙に CSS や JS の挙動が変わるクロスブラウザ問題が発生するため、そのための対応やテストも必要になります。機能が少ないうちは WebView でもいいかもしれませんが、機能が増えてくるとテストの手間も何倍にもなっていきます。

Deno Desktop の場合、最初は軽量な WebView でスタートし、クロスブラウザが重荷になってきたら、ちょっと配布サイズは大きくなるけど、CEF にスイッチできるのがいいかなと思います。

Information

Tauri だとこの辺、Servo ベースの自前 WebView プロジェクト Verso 待ちですが、Deno は既存の Chromium を選択可能にしているあたり、現時点での割り切りが感じられますね。

Electron との比較

#

既存 Web アプリをデスクトップ化したいユースケースでは、Deno Desktop はかなり有力です。

一方で、Electron の WebContentsView のような高度なマルチビュー構成を前提にしている場合は、現時点では Electron のほうが適しています。たとえば VS Code や Figma のように、複数ビューを細かく制御するタイプのアプリです。

ざっくり整理すると次のような感触です。

  • 単一ウィンドウ中心 + 既存 Web 資産活用: Deno Desktop はかなり良い
  • 複雑なウィンドウ/ビュー管理: Electron が依然強い
Information

マルチビュー構成の対応の弱さは Tauri も同様です。

Electron の WebContensView 構成については以下の記事をご参照ください。

さいごに

#

以上、Deno Desktop 機能を一通り試しました。

Out of the box でここまでデスクトップ開発体験が整っているのは率直に驚きです。タスクトレイやメニュー、Bindings など、アプリらしさを出すための API が最初から揃っているのも好印象でした。

Tauri と違ってアプリ側をすべて TypeScript で書けるため、既存 Web アプリをベースに「メニューやタスクトレイを追加し、OS 機能と連携する」用途ではかなり相性がよいと感じます。最小構成なら、デスクトップアプリ化自体は1時間もかからないはずです。

Information

Tauri も JS の API を生やして、Rust 知らない勢を取り込もうとしてはいます。

Deno のキラー機能になる可能性もありますね。experimental から安定版へ向けて、今後の熟成がとても楽しみな機能です。


  1. 2026年7月6日現在の最新は 2.9.1 です。 ↩︎

豆蔵では共に高め合う仲間を募集しています!

recruit

具体的な採用情報はこちらからご覧いただけます。