Sik.limited Logo

Webコードでアプリを作る: Tauri・Electron・Capacitorの選び方

WebアプリをiOS・Android・Windows・macOSへ広げる際に、Tauri、Electron、Capacitorのどれを選ぶかを、製品要件、権限、WebView、セキュリティ、配布で整理します。

Sik ·

フレームワークから選ぶと、肝心の質問を見落とす

Webで先に作った製品をアプリに広げるとき、Tauri・Electron・Capacitorは一行の比較表で語られがちです。「軽いのはTauri」「成熟しているのはElectron」「モバイルならCapacitor」で終えることもできます。しかし実際の選択はライブラリ名より、ユーザーがどの画面で、どんなリスクを引き受け、どの速度で更新を受け取るかに近いものです。

社内だけで使うファイル整理ツールと、iPhoneのカメラ・プッシュ通知を毎日使う消費者向けサービスは、同じWebコードから始めても運用の中心が違います。前者ではファイル権限とデスクトップ配布、後者ではストア審査、通知権限、オフライン復帰が先に来ます。アプリの外側を選ぶことは、製品が守る約束を選ぶことでもあります。

この記事は三つの優劣を決めるものではありません。企画、デザインQA、開発、配布が同じ文書で判断できる基準を置きます。すでにTauri vs Electron: SvelteKitデスクトップアプリの選択基準Capacitor vs Tauri: 一つのWebコードベースをモバイル・デスクトップアプリへ拡張する選択基準を読んだなら、本稿はその比較をリリース計画につなぐハブです。

三つのツールが包む境界は異なる

「Webアプリをネイティブアプリにする」を分解しましょう。三つともHTML、CSS、JavaScriptで作った画面を活用できますが、同じ種類のランタイムではありません。

問いCapacitorTauriElectron
出発点モバイルWebアプリをiOS・Androidアプリへ接続WebフロントエンドとRustのネイティブコアを接続WebフロントエンドとChromium・Node.jsを同梱して配布
最も自然な場iOS・AndroidWindows・macOS・LinuxのデスクトップWindows・macOS・Linuxのデスクトップ
画面エンジン各モバイルOSのWebViewOSのWebViewアプリ同梱のChromium
ネイティブ機能との接点プラグインとiOS/Androidプロジェクト明示したcommand・plugin capabilitymain/preload/rendererとIPC
最初に設計するもの権限、プラグイン、ストアビルド権限範囲、Rust境界、OS別UI QAIPC境界、セキュリティ設定、パッケージ化と更新

Capacitorはモバイルのネイティブプロジェクトを管理する流れに近く、TauriとElectronはデスクトップ配布を中心にした選択です。Tauriにもモバイル対応があり、Capacitorのコードにデスクトップターゲットを足す組合せもあります。ただし出発点と運用コストを同列に置くと、初期に節約した時間をQAとリリースで払い直すことになります。

共有できる領域
  ├─ ドメインモデル・APIクライアント・デザイントークン・画面コンポーネント
  ├─ Web配布物(PWA / ブラウザ)
  └─ プラットフォームアダプター
      ├─ Capacitor: iOS / AndroidプラグインとWebView
      ├─ Tauri: Rust command / capability / システムWebView
      └─ Electron: preload / IPC / Chromium + Node.js

重要なのは共有コードの量ではなく、共有してよい責務の量です。決済、カメラ、ファイル、プッシュ、バックグラウンド処理など、プラットフォームが端末に直接触れる地点は薄いアダプターとして残します。画面コードが同じだからといってこの境界まで隠すと、障害時に誰がどの権限で何を呼んだかを追えなくなります。


製品要件を先に四枚へ分ける

技術会議の前に、次の四枚を書けばフレームワーク論争は短くなります。

  1. 利用環境: ユーザーは机の前の大画面で作業するのか、移動中にスマートフォンを開くのか、両方か。
  2. 端末機能: カメラ、写真ライブラリ、プッシュ、生体認証、共有シートが中核か。あるいはファイルシステム、トレイ、複数ウィンドウ、グローバルショートカットが中核か。
  3. 信頼境界: ローカルファイル、認証情報、会社データ、ユーザー生成HTMLのうち、何を読み書きするか。
  4. リリース速度: サーバーだけで変えられるコンテンツと、ストア審査またはインストーラー更新が要る実行コードを分けたか。

これは企画書であると同時にアーキテクチャ文書です。「モバイルも後で対応」はまだ要件ではありません。たとえば写真を撮影してOCRへ送り、プッシュで結果を受け、オフラインでは一時保存するなら、Capacitor側のプラグイン、権限、復帰フローが製品範囲です。会議中に多数のファイルをドラッグし、メニューバーから起動し、キーボードで編集するなら、デスクトップは単なる画面サイズの拡張ではありません。

すばやい選択質問

この問いに「はい」なら出発点
今四半期の中心がiOS・Android公開で、カメラ、プッシュ、共有が機能の一部かCapacitor
デスクトップアプリが中核で、ローカル権限を最小限に宣言しRust境界を置けるかTauri
Chromiumの一貫した描画、Node.jsのエコシステム、既存Electron資産が重要かElectron
モバイルとデスクトップの両方が必要かモバイル軸にCapacitorを置き、デスクトップ要件はTauriまたはElectronを別途評価
どれも明確でないか先にレスポンシブWeb/PWAで中核行動を検証し、ネイティブ要件を記録

「全プラットフォームを一度に」は要件ではなく願望であることが多いものです。まず一つのプラットフォームで完結した体験を作り、次のプラットフォームで共有するものと分けるものを確かめてから開く方がよいでしょう。

CapacitorはモバイルWebViewの問題を製品問題にする

Capacitorは、Webアプリへネイティブ機能を加えるチームに自然です。Web資産をビルドし、iOS・Androidプロジェクトへ同期し、プラグインでカメラ、通知、ファイルを呼びます。既存の画面、状態管理、APIレイヤーを大きく捨てずに済むのが利点です。

一方、モバイルアプリになった瞬間、ブラウザでは見えなかった状態が増えます。権限を拒否した画面、バックグラウンドから戻る瞬間、遅いネットワークでアップロードが切れる瞬間、審査者がログインできない導線までが製品です。

// src/platform/camera.ts
export type CapturedPhoto = { dataUrl: string; format: 'jpeg' | 'png' };

export async function capturePhoto(): Promise<CapturedPhoto> {
  if (typeof window === 'undefined') {
    throw new Error('ブラウザのレンダリング中はカメラを開けません。');
  }
  const { Camera, CameraResultType } = await import('@capacitor/camera');
  const result = await Camera.getPhoto({
    quality: 85, resultType: CameraResultType.DataUrl, allowEditing: false
  });
  if (!result.dataUrl || !result.format) throw new Error('写真データを取得できませんでした。');
  return { dataUrl: result.dataUrl, format: result.format as 'jpeg' | 'png' };
}

この例で重要なのはCamera APIではなく、画面コンポーネントがプラグインを直接読み込まないことです。Webプレビューには代替UIを表示でき、テストには同じ型を返す偽実装を入れられます。

npm run build
npx cap sync
npx cap open ios
# または
npx cap open android

syncは単なるコピーではなく、Webビルド結果とプラグイン設定をネイティブプロジェクトへ反映する境界です。CIで抜けると、ローカルの最新画面とストアへ出すバイナリのWeb資産がずれます。リリースパイプラインではWebビルド番号とiOS/Androidのバージョンを共に出力します。

Tauriは小さなバンドルより小さな権限に意味がある

TauriはWebフロントエンドとRustコアを結び、各OSのWebViewを使います。バンドルサイズやメモリ値だけを比較の出発点にすると、本当の価値を見落とします。価値はフロントエンドがOSへ何を要求できるかを明示的に狭める構造にあります。

// src-tauri/src/lib.rs
#[tauri::command]
fn export_report(destination: String, body: String) -> Result<(), String> {
    if !destination.ends_with(".md") {
        return Err("Markdownファイルだけを出力できます。".into());
    }
    std::fs::write(destination, body)
        .map_err(|error| format!("ファイルを保存できませんでした: {error}"))
}
// src-tauri/capabilities/default.json
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "main-window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "dialog:allow-save",
    { "identifier": "fs:allow-write-text-file", "allow": [{ "path": "$HOME/Documents/my-app/**" }] }
  ]
}

実際のpermission識別子とスキーマは、使っているプラグイン版の公式文書で確認してください。この例は保存ボタン一つにも、ファイル選択、書込み可能なパス、commandが受ける入力という三つの決定があることを示します。三つを一度に広く開かないことが重要です。

システムWebViewは利点である一方、検証項目でもあります。Windows、macOS、Linuxでは描画エンジンと更新方式が異なり得ます。デザインQAはOS別の実機、少なくとも該当エンジンで行います。「Chromeで見える」は合格条件ではありません。フォント改行、ドラッグ&ドロップ、メディア、編集機能、最新CSSのような中核画面を先にテスト一覧へ入れます。

Electronは重さの反対ではなく一貫性を選ぶもの

ElectronはChromiumとNode.jsをアプリに同梱します。よってインストール物と更新対象のランタイムが明確です。その代わり複数OSで同じChromiumベースの画面を試せ、JavaScript・Node.jsに慣れたチームはバックエンド境界をRustで作り直さずに済みます。

ただしrendererへNode.js権限を丸ごと渡してはいけません。preloadで必要な機能だけを公開するのが基本です。

// electron/preload.ts
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('desktop', {
  pickExportPath: () => ipcRenderer.invoke('dialog:pick-export-path'),
  saveTextFile: (path: string, contents: string) =>
    ipcRenderer.invoke('file:save-text', { path, contents })
});
// electron/main.ts
import { dialog, ipcMain } from 'electron';
import { writeFile } from 'node:fs/promises';
ipcMain.handle('dialog:pick-export-path', async () => {
  const result = await dialog.showSaveDialog({ filters: [{ name: 'Markdown', extensions: ['md'] }] });
  return result.canceled ? null : result.filePath;
});
ipcMain.handle('file:save-text', async (_event, input: { path: string; contents: string }) => {
  if (!input.path.endsWith('.md')) throw new Error('Markdownファイルだけを保存できます。');
  await writeFile(input.path, input.contents, 'utf8');
});

画面が得るのは狭いdesktop.saveTextFileだけであり、require('fs')や任意のNode APIではありません。この制約は少し手間ですが、画面のXSSを直ちに端末権限の問題へ拡大させない重要な境界です。

画面は共有しても、機能を無理に共有しない

三つのランタイムを並行運用するときの失敗は、「コードベース一つ」を「動作一つ」と誤解することです。共有部分と各プラットフォームに残す部分を初めから分けます。

packages/
  core/             # ドメイン型、API、バリデーション
  ui/               # デザイントークン、画面コンポーネント
  web/              # ブラウザ用エントリポイント
  mobile/           # Capacitor初期化、モバイル権限・プラグイン
  desktop-tauri/    # Tauri command、capability、バンドル設定
  desktop-electron/ # preload、IPC、パッケージ設定

coreには「写真をアップロードする」「レポートを出力する」という製品意図を置きます。mobileにはカメラ権限の確認方法を、デスクトップ側にはファイル保存という意図をOS APIへ変える方法を置きます。権限拒否、更新待ち、オフライン失敗の状態まで共通コンポーネントで扱うと、デザインシステムの効果はさらに大きくなります。

性能はフレームワークではなく作業の移動経路から生まれる

TauriとElectronの比較ではインストールサイズとメモリがよく話題になります。しかし製品の実際のボトルネックは、大きな画像をどこでデコードするか、仮想化していない一覧、同期API、頻繁すぎるIPC、UIを止めるバックグラウンド処理にあることが少なくありません。性能仮説はフレームワーク名ではなくユーザー行動で書きます。

ユーザー行動測るもの改善の方向
初回起動最初の画面が操作可能になる時点初期データと重いモジュールを遅延ロード
1,000ファイルの閲覧スクロール・検索中のフレームと応答仮想リスト、索引処理の分離
撮影・アップロード権限から完了案内まで再試行とバックグラウンド復帰状態
更新の受信ダウンロード、再起動、ロールバックリリースチャネルと復旧経路

ネイティブ境界を越える呼出しはAPIとして扱います。一文字入力ごとにTauri commandやElectron IPCを呼ぶのではなく、入力はローカル状態で処理し、保存、索引、ファイル操作の瞬間だけ束ねます。

// 悪い例: 入力ごとにデスクトップ境界を越える
editor.on('change', (value) => window.desktop.saveDraft(value));

let pending: ReturnType<typeof setTimeout> | undefined;
editor.on('change', (value) => {
  clearTimeout(pending);
  pending = setTimeout(() => saveDraft(value), 700);
});
async function saveDraft(value: string) {
  await draftRepository.save({ value, savedAt: new Date().toISOString() });
}

選択を検証する小さなスパイクを作る

フレームワークを決める前に、製品で最も危険な一場面を候補ランタイムで最後まで通す小さなスパイクを作ります。目的はきれいな初期画面ではなく、後から戻しにくい前提を早く壊すことです。

ファイル中心のデスクトップ製品なら、フォルダー選択、一覧読込み、一ファイルの編集・保存、再起動後の安全な復元を通します。拒否、キャンセル、権限なし、同時編集までを画面で確認します。モバイル製品ならカメラやプッシュを選び、初回権限、拒否後の代替導線、バックグラウンド完了の再開、遅いネットワークの再試行を続けて試します。

確認項目合格条件失敗時に残す記録
ビルドCIのクリーン環境で成果物を生成SDK・ランタイム版とエラーログ
描画中核画面が対象OSで利用可能OS、WebView/Chromium版、スクリーンショット
権限拒否・キャンセル後にも次の行動が見えるユーザーが止まった段階と文言
データ再起動・更新後も下書きを保持フォーマット版と復旧方法
配布インストール、削除、再インストールが想定通りインストール先、署名、残るデータ

リリース前には機能ではなく失敗経路をリハーサルする

マーケットまたはダウンロードページへ出す前に、正常動作だけをデモしても重要な問題は残ります。権限拒否時に理由と設定への戻り道があるか、ネットワーク断で未保存作業を失わないか、再起動時のアップロード・下書きはどうなるか、更新直後の移行に失敗したとき戻れるか、古いOSと異なるWebViewで何が制限されるかを確認します。

TauriとElectronのデスクトップ更新、App StoreとPlay Storeのモバイル更新は、同じ「新バージョン」でも制御主体が異なります。Webアプリの更新戦略: App Store・Play Store・Tauri・Electronの配布をどう分けるかでは、これをリリースチャネル、署名鍵、緊急修正、ロールバックまでつなげます。

選択は一度の宣言ではなく、戻せる決定である

小さなチームが避けるべきなのは、将来の全要求を想像して最も複雑な構造を先に作ることです。現在の中核行動を一つ選び、それが最も自然なプラットフォームで最後まで完結することを確認します。

  • モバイルのカメラ、プッシュ、共有が中核なら、Capacitorで権限とストアフローまで完成させます。
  • ローカルファイル、ウィンドウ、キーボード、バックグラウンド処理が中核なら、TauriまたはElectronでインストール、更新、復旧まで完成させます。
  • すでにElectronを運用しているなら、「新しいものの方が軽い」だけで移行せず、解決する費用を数値と再現シナリオで書きます。
  • Tauriを選ぶなら、Rustを導入した事実でなく、capabilityとcommandをどれだけ狭く設計したかで品質を判断します。

良い選択は、後で別のランタイムを使えなくする選択ではありません。画面と製品規則は共有し、端末権限と配布は薄い境界として残すことで、次の決定の費用を下げる選択です。

公式ドキュメント

最新の記事