Electronのメモリ消費に絶望してFlutter Desktopを本番投入した話:アーキテクチャ比較と移行の判断基準

「たかだか社内用のCRUDツールなのに、なぜアイドル状態でメモリを600MBも食っているんだ?」

これは、私が半年前に開発したElectron製デスクトップアプリに対して、運用チームから投げかけられた最初のクレームでした。当時、私たちはWindows 10/11とmacOSが混在する社内環境向けに、レガシーな業務システムをラップするデスクトップクライアントを開発していました。Web技術(React + TypeScript)をそのまま転用できるElectronは、初期開発においては魔法のような生産性を発揮しました。しかし、配布後の「実際の現場」では、Core i3 / 8GB RAMといった標準的なビジネスPCにおいて、ブラウザ(Chrome)とElectronアプリを同時に開くと動作がもたつくという、致命的なリソース競合が発生していたのです。

Chromiumを抱える重罪:アーキテクチャの根源的な違い

この問題を解決するために、私たちは次期バージョンの技術選定としてFlutter Desktopの検証を開始しました。まず理解すべきは、ElectronとFlutterのレンダリングパイプラインの決定的な違いです。

Electronの本質は「ChromiumブラウザとNode.jsランタイムの同梱」です。つまり、アプリを一つ起動するたびに、独立したブラウザインスタンスとV8エンジンが立ち上がります。これはOSの上にさらにOSを載せているようなもので、どれだけコードを最適化しても、Chromium自体のベースラインメモリ(Hello Worldレベルでも100MB前後)からは逃れられません。

現場でのログ: Electron (v28.0) でメインプロセスとレンダラープロセスが分離された際、IPC通信のオーバーヘッドにより、大量のデータグリッド描画時にUIスレッドが150ms以上ブロックされる現象を確認。

一方、Flutterは全く異なるアプローチを取ります。FlutterはSkia(現在はImpellerへ移行中)というレンダリングエンジンを内包し、UIをOSのネイティブキャンバスに直接描画します。DartコードはAOT(Ahead-of-Time)コンパイルされ、ネイティブのマシン語として実行されます。ブリッジを介してDOMを操作するのではなく、GPUを直接叩いてピクセルを描画するため、「ゲームエンジンのような挙動」をします。

Node.jsエコシステムへの未練と失敗

移行検討の初期段階で、私たちが犯した最大の失敗は「Electronのバックエンドロジック(Node.js)をそのまま維持しようとしたこと」でした。Flutter側からローカルサーバーとしてNode.jsプロセスを起動し、HTTP通信でやり取りする構成を試みました。

しかし、これは「Electronの悪いところ(Node.jsのランタイム配布)」と「Flutterの悪いところ(バイナリサイズ)」を合体させたキメラを生み出すだけでした。配布サイズは200MBを超え、起動時間はむしろ悪化しました。Flutterに移行するならば、ビジネスロジックもDartで書き直し、必要なネイティブ機能はFFI(Foreign Function Interface)で呼ぶという「Dartネイティブ」な思想に完全に切り替える必要があります。

IPC通信とFFI:コードレベルでの比較

ElectronとFlutterの最大の違いは、ネイティブ機能(OSのAPI)へのアクセス方法に現れます。ElectronではIPC(Inter-Process Communication)を使いますが、これは非同期かつ直列化(シリアライズ)のコストがかかります。

以下は、Electronでの一般的なIPCハンドラーの実装例です。

// Electron: main.ts (メインプロセス)
import { ipcMain } from 'electron';

// JSONシリアライズ可能なデータしか送れない
ipcMain.handle('perform-heavy-calc', async (event, data) => {
  const result = heavyCalculation(data); 
  return result;
});

// Electron: preload.ts (レンダラープロセスへのブリッジ)
import { contextBridge, ipcRenderer } from 'electron';

contextBridge.exposeInMainWorld('api', {
  calc: (data: any) => ipcRenderer.invoke('perform-heavy-calc', data)
});

大量のデータをやり取りする場合、このJSONシリアライズ/デシリアライズがボトルネックになります。対して、Flutter(Dart)のFFIを使用すると、C/C++やRustで書かれたネイティブライブラリをメモリ共有した状態で直接呼び出すことが可能です。これは特に画像処理や暗号化処理で劇的な差を生みます。

// Flutter: Dart FFI (外部のRust/Cライブラリを直接叩く)
import 'dart:ffi' as ffi;
import 'package:ffi/ffi.dart';

// Cの関数シグネチャを定義
typedef NativeAddFunc = ffi.Int32 Function(ffi.Int32 a, ffi.Int32 b);
typedef DartAddFunc = int Function(int a, int b);

void main() {
  // 共有ライブラリをロード(オーバーヘッドほぼゼロ)
  final dylib = ffi.DynamicLibrary.open('native_lib.dll');
  
  // 関数ポインタを取得
  final add = dylib
      .lookup<ffi.NativeFunction<NativeAddFunc>>('add')
      .asFunction<DartAddFunc>();

  // ネイティブ速度で実行
  print(add(2, 3)); 
}

このように、Flutterでは「OSとの壁」が非常に薄いのが特徴です。私たちは、Rustで書かれた高速なデータ処理モジュールをFlutter FFI経由で呼び出す構成に変更し、Electron時代と比較して処理速度を約4倍に向上させました。

指標Electron (v28)Flutter (v3.19)
アイドル時メモリ~180 MB~45 MB
インストーラーサイズ~120 MB~35 MB
起動時間 (コールド)1.8秒0.6秒
描画FPS (リスト1万件)45fps (かくつき有)60fps (安定)

ベンチマークの結果は一目瞭然でした。特にメモリ使用量の削減は劇的で、古いPCを使用しているユーザーからの「PCが重くなる」というクレームは完全に消失しました。ElectronがChromiumプロセスを維持するために消費していたリソースが、そのままユーザー体験の向上に還元された形です。

Flutter Desktop 公式ドキュメントを確認

Flutter Desktopの落とし穴:採用してはいけないケース

しかし、全てのケースでFlutterがElectronに勝るわけではありません。移行して痛感した「Flutter Desktopの弱点」についても公平に記しておきます。

まず、リッチテキストエディタの実装は地獄です。Electronであれば、既存の優れたライブラリ(ProseMirrorやQuill.jsなど)や、ブラウザ標準の`contenteditable`をそのまま利用できます。しかし、Flutterで同等の機能を実現しようとすると、IME(日本語入力)の制御を含め、レンダリングを一から制御する必要があり、既存のパッケージもまだWebほど成熟していません。

また、サードパーティライブラリの依存関係も課題です。Node.js (NPM) には無数のライブラリが存在し、AWS SDKやDBドライバなどが公式に提供されていますが、Dartのpub.devはまだ発展途上です。特に、デスクトップ固有のネイティブ機能(特定のBluetoothプロファイルや古いプリンタードライバ制御など)が必要な場合、自分でC++やRustのブリッジを書く工数が発生する可能性が高いです。

推奨: SEOが必要なWebアプリとコードを共有したい場合や、既存のWeb資産が膨大な場合は、依然としてElectron(またはTauri)が有力な選択肢です。

結論:パフォーマンス重視ならFlutterへの投資は裏切らない

ElectronからFlutterへの移行は、単なるフレームワークの変更ではなく、アプリのアーキテクチャそのものの再設計を意味しました。学習コストやライブラリの不足という課題はありましたが、結果として得られた「軽快な動作」と「堅牢な型安全性(Dart)」は、長期的な保守コストを大幅に引き下げてくれました。

もしあなたのチームが、Electronアプリのパフォーマンスチューニングに限界を感じているなら、部分的にでもFlutterによるプロトタイピングを試してみる価値は十分にあります。

Post a Comment