取り組みの背景:デスクトップ AI アプリの復活
クラウドベースの AI サービスが主流になった一方で、プライバシー・オフライン対応・低レイテンシー を求めるユースケースが急速に復活しています。
テキストエディタの Cursor や IDE の Windsurf といった Electron ベースの AI 開発ツール、さらには個人・企業向けのローカル AI アシスタントアプリの需要が高まっています。デスクトップアプリケーションは、ウェブアプリよりも以下の利点があります:
- 完全なプライバシー: ユーザーデータがマシン上に留まり、クラウドに送信されない
- オフライン対応: インターネット接続なしに AI モデルを実行可能
- 低レイテンシー: GPU/NPU への直接アクセス、最小限のネットワーク遅延
- システム統合: OS レベルのファイルアクセス、トレイ、ホットキー、ネイティブ機能
- 永続的な学習: ローカル埋め込みモデルによる個人化とコンテキスト保持
ここで整理するのはElectron と Antigravity を組み合わせて、本格的なデスクトップ AI アプリケーションを構築する方法 を段階的に解説します。
Electron × AI アプリのアーキテクチャ設計
デスクトップ AI アプリを堅牢に設計するには、マルチプロセスモデル と 責任分離 の理解が不可欠です。
Electron のプロセスモデル
Electron は以下の 3 層構造で動作します:
1. メインプロセス(Nodeフルアクセス)
- ウィンドウ、メニュー、トレイの管理
- ファイルシステム、ネイティブモジュールへのアクセス
- IPC を通じたレンダラープロセスとのM通信
- アプリケーションのライフサイクル
2. レンダラープロセス(サンドボックス内)
- UI レンダリング(React、Vue など)
- ユーザーインタラクション処理
- メインプロセスへの IPC リクエスト
3. ワーカープロセス(オプション)
- GPU 処理、重い計算
- メインプロセスのブロッキングを回避
- LLM 推論、画像処理など
AI 統合アーキテクチャの全体像
┌─────────────────────────────────────────────┐
│ UI Layer (React/Vue) │
│ ┌─────────────────────────────┐ │
│ │ Chat Interface │ │
│ │ File Drop Zone │ │
│ │ Settings Panel │ │
│ └──────────┬──────────────────┘ │
└─────────────────┼──────────────────────────┘
│ IPC (async/await)
┌─────────────────▼──────────────────────────┐
│ Main Process │
│ ┌──────────────────────────────┐ │
│ │ IPC Handler Registry │ │
│ │ Model Manager │ │
│ │ DB Connection │ │
│ │ FS Operations │ │
│ └──────────────────────────────┘ │
└─────────────────┬──────────────────────────┘
│
┌─────────┼─────────┐
│ │ │
┌───▼──┐ ┌───▼──┐ ┌────▼───┐
│LLM │ │Embed │ │Vision │
│Model │ │Model │ │ Module │
└──────┘ └──────┘ └────────┘
このアーキテクチャにより、UI のブロッキングを回避し、AI 処理の進行状況をリアルタイムで UI に反映できます。
プロジェクトスキャフォールディングと Antigravity 開発フロー
Antigravity を使用することで、Electron × Node.js × Python(AI)の複雑なプロジェクト構造を効率的に管理できます。
推奨プロジェクト構成
electron-ai-app/
├── src/
│ ├── main/ # メインプロセス
│ │ ├── ipc/ # IPC ハンドラー
│ │ │ ├── model.ts
│ │ │ ├── file.ts
│ │ │ └── system.ts
│ │ ├── services/ # AI サービス層
│ │ │ ├── llm.ts
│ │ │ ├── embedding.ts
│ │ │ └── inference.ts
│ │ └── main.ts # エントリーポイント
│ ├── renderer/ # レンダラープロセス
│ │ ├── components/ # React コンポーネント
│ │ ├── hooks/ # カスタムフック
│ │ ├── pages/
│ │ └── App.tsx
│ └── shared/ # 共有型定義
│ ├── types.ts
│ └── constants.ts
├── python/ # AI モデル・推論スクリプト
│ ├── inference.py
│ ├── embedding.py
│ └── requirements.txt
├── resources/ # 静的リソース
│ ├── icons/
│ └── models/ # LLM ウェイト
├── vite.config.ts
├── electron-builder.json
└── package.json
Antigravity でのコーディングフロー
Antigravity の AI アシスト機能を活用して、以下のスピードアップを実現:
-
型安全な IPC インターフェース自動生成
shared/types.ts からレンダラー ↔ メインプロセス型定義を自動推論
- Antigravity が IPC ハンドラースタブを自動提案
-
マルチファイル編集の一括リファクタリング
- メインプロセスの型変更時、全 IPC ハンドラーを Antigravity が同時に更新提案
- 手動対応時間を 80% 削減
-
Python ↔ Node.js ブリッジの自動補完
- Python のモデル出力型と Node.js の型定義をマッピング
- Antigravity が child_process 呼び出し生成
-
エラーハンドリングテンプレートの自動展開
- IPC ハンドラーを書くたび、Antigravity が try-catch + ログ構造を提案
- 一貫性のあるエラーハンドリング実装を加速
具体的な作業フロー:
1. Antigravity のコマンドパレット: "Generate IPC Handler"
↓
2. ハンドラー名・入出力型を入力
↓
3. Antigravity が以下を自動生成:
- main/ipc/xxx.ts(ハンドラー)
- renderer/hooks/useXxx.ts(フック)
- shared/types.ts(型定義)
↓
4. レンダラーコンポーネントで useIpc('xxx') を呼び出すだけ
このフローにより、複雑なマルチプロセス構造も開発速度を損なわずに実装できます。
メインプロセスとレンダラープロセスの IPC 設計
IPC の基本パターン
Electron IPC は、invoke(リクエスト-レスポンス) と send(一方向通知) の 2 モードで動作します。
パターン 1: Invoke(レスポンス待機型)
// main/ipc/llm.ts
ipcMain.handle('llm:generate', async (event, prompt: string) => {
try {
const response = await llmService.generate(prompt)
return { success: true, data: response }
} catch (error) {
return { success: false, error: error.message }
}
})
// renderer/hooks/useLLM.ts
async function generateText(prompt: string) {
const result = await ipcRenderer.invoke('llm:generate', prompt)
if (result.success) {
setResponse(result.data)
} else {
setError(result.error)
}
}
パターン 2: Send(ストリーミング型)
// main/ipc/llm.ts - ストリーミング推論
ipcMain.on('llm:stream', async (event, prompt: string) => {
const stream = llmService.generateStream(prompt)
for await (const chunk of stream) {
event.sender.send('llm:stream:chunk', chunk)
}
event.sender.send('llm:stream:done')
})
// renderer/components/ChatBox.tsx
useEffect(() => {
ipcRenderer.on('llm:stream:chunk', (chunk) => {
setMessages(prev => [...prev, chunk])
})
return () => ipcRenderer.removeAllListeners('llm:stream:chunk')
}, [])
IPC タイムアウトとエラーハンドリング
AI 推論は数秒~数分かかるため、タイムアウト管理 が必須です。
// main/services/inference.ts
const INFERENCE_TIMEOUT = 5 * 60 * 1000 // 5分
async function invokeWithTimeout<T>(
name: string,
args: unknown[],
timeout: number = INFERENCE_TIMEOUT
): Promise<T> {
return Promise.race([
ipcRenderer.invoke(name, ...args),
new Promise((_, reject) =>
setTimeout(
() => reject(new Error(`${name} timed out after ${timeout}ms`)),
timeout
)
),
])
}
// renderer での使用
try {
const result = await invokeWithTimeout('llm:generate', [prompt], 30000)
} catch (error) {
if (error.message.includes('timed out')) {
showNotification('推論がタイムアウトしました。プロンプトを短くしてください。')
}
}
双方向バインディングと状態共有
メインプロセスのモデル状態(読み込み中、準備完了など)をレンダラーにリアルタイム配信:
// main/services/model-manager.ts
class ModelManager extends EventEmitter {
private models: Map<string, AIModel> = new Map()
async loadModel(modelId: string) {
this.emit('model:loading', modelId)
try {
const model = await downloadAndCacheModel(modelId)
this.models.set(modelId, model)
this.emit('model:loaded', modelId)
} catch (error) {
this.emit('model:error', { modelId, error: error.message })
}
}
}
// main/ipc/model.ts
modelManager.on('model:loading', (modelId) => {
mainWindow.webContents.send('model:status', { modelId, status: 'loading' })
})
// renderer/hooks/useModelStatus.ts
export function useModelStatus() {
const [models, setModels] = useState({})
useEffect(() => {
ipcRenderer.on('model:status', (status) => {
setModels(prev => ({
...prev,
[status.modelId]: status.status
}))
})
}, [])
return models
}
ローカル AI モデルの組み込みと推論パイプライン
モデルの配布戦略
ローカル LLM を使用する場合、モデルファイルサイズ が大きな課題になります。推奨アプローチ:
オプション 1: 遅延ダウンロード(推奨)
- アプリ起動時はモデルをダウンロードしない
- ユーザーが初回使用時にオンデマンドダウンロード
- キャッシュは
app.getPath('userData')/models/ に保存
- ダウンロード進捗を UI に表示
オプション 2: アプリバンドル(小型モデルのみ)
- 量子化済みモデル(GGUF フォーマット)のみを内包
- アプリサイズ増加を覚悟 → 安定性重視の場合に有効
オプション 3: オンライン推論フォールバック
- ローカルモデル実行失敗時、クラウド API に自動フェイルオーバー
- ユーザーの利便性を最優先
// main/services/model-manager.ts
const MODEL_CACHE_DIR = path.join(app.getPath('userData'), 'models')
async function ensureModelExists(modelId: string): Promise<string> {
const modelPath = path.join(MODEL_CACHE_DIR, modelId, 'model.gguf')
// キャッシュに存在する場合、スキップ
if (fs.existsSync(modelPath)) {
return modelPath
}
// ダウンロード開始
mainWindow.webContents.send('model:download:start', { modelId })
try {
await downloadModelWithProgress(modelId, modelPath, (progress) => {
mainWindow.webContents.send('model:download:progress', {
modelId,
progress, // 0-100
})
})
mainWindow.webContents.send('model:download:complete', { modelId })
return modelPath
} catch (error) {
mainWindow.webContents.send('model:download:error', {
modelId,
error: error.message,
})
throw error
}
}
Python 推論エンジンの統合
Node.js だけでは大規模な LLM を実行できないため、Python + llama-cpp-python を子プロセスで実行:
// main/services/inference.ts
import { spawn } from 'child_process'
import { EventEmitter } from 'events'
class LLMInference extends EventEmitter {
private pythonProcess: ChildProcess | null = null
async initialize(modelPath: string) {
this.pythonProcess = spawn('python', ['python/inference.py', modelPath], {
stdio: ['pipe', 'pipe', 'pipe'],
env: {
...process.env,
CUDA_VISIBLE_DEVICES: '0', // GPU インデックス
PYTHONUNBUFFERED: '1',
},
})
this.pythonProcess.stdout!.on('data', (data) => {
const message = JSON.parse(data.toString())
if (message.type === 'ready') {
this.emit('ready')
}
})
}
async generate(prompt: string, options: GenerateOptions) {
return new Promise((resolve, reject) => {
const requestId = crypto.randomUUID()
this.pythonProcess!.stdin!.write(
JSON.stringify({ requestId, prompt, options }) + '\n'
)
const handleMessage = (data) => {
const message = JSON.parse(data.toString())
if (message.requestId === requestId) {
if (message.type === 'complete') {
this.pythonProcess!.stdout!.removeListener('data', handleMessage)
resolve(message.output)
} else if (message.type === 'error') {
reject(new Error(message.error))
}
// 'chunk' イベントは UI に ストリーミング送信
this.emit('chunk', message)
}
}
this.pythonProcess!.stdout!.on('data', handleMessage)
})
}
}
Python 推論スクリプト (python/inference.py):
import json
import sys
from llama_cpp import Llama
model_path = sys.argv[1]
llm = Llama(model_path, n_gpu_layers=-1, verbose=False)
# 起動完了通知
sys.stdout.write(json.dumps({"type": "ready"}) + "\n")
sys.stdout.flush()
# リクエスト処理ループ
for line in sys.stdin:
request = json.loads(line)
request_id = request['requestId']
prompt = request['prompt']
options = request.get('options', {})
try:
output = ""
# ストリーミング生成
for chunk in llm(
prompt,
max_tokens=options.get('max_tokens', 256),
temperature=options.get('temperature', 0.7),
stream=True,
):
token = chunk['choices'][0]['text']
output += token
# チャンク送信
sys.stdout.write(json.stringify({
'requestId': request_id,
'type': 'chunk',
'token': token,
}) + "\n")
sys.stdout.flush()
# 完了通知
sys.stdout.write(json.stringify({
'requestId': request_id,
'type': 'complete',
'output': output,
}) + "\n")
sys.stdout.flush()
except Exception as e:
sys.stdout.write(json.stringify({
'requestId': request_id,
'type': 'error',
'error': str(e),
}) + "\n")
sys.stdout.flush()
ネイティブモジュール統合(Node.js Addon / N-API)
ネイティブモジュールが必要な場合
JavaScript のパフォーマンスが十分でない場合、C/C++ ネイティブモジュールを統合できます:
- GPU 処理: CUDA/Metal による行列演算の高速化
- リアルタイムオーディオ: ノイズ除去、音声認識の低遅延実装
- 画像処理: OpenCV による高速フィルタリング
- ハードウェア統合: USB デバイス、ハードウェアアクセラレーター
N-API を使用した簡単な統合
// native/src/gpu_buffer.cc
#include <napi.h>
#include <cuda_runtime.h>
Napi::Value AllocateGPUBuffer(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() < 1) {
Napi::TypeError::New(env, "Expected 1 argument")
.ThrowAsJavaScriptException();
return env.Null();
}
size_t size = info[0].As<Napi::Number>().Uint32Value();
float* device_ptr;
cudaMalloc(&device_ptr, size * sizeof(float));
// ポインタをバッファハンドルとして返す
return Napi::Buffer<float>::New(env, device_ptr, size);
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set(
Napi::String::New(env, "allocateGPUBuffer"),
Napi::Function::New(env, AllocateGPUBuffer)
);
return exports;
}
NODE_API_MODULE(gpu_buffer, Init)
binding.gyp:
{
"targets": [
{
"target_name": "gpu_buffer",
"sources": ["src/gpu_buffer.cc"],
"include_dirs": ["<!@(node -p \"require('node-addon-api').include\")"],
"dependencies": ["<!(node -p \"require('node-addon-api').gyp\")"],
"cflags_cc": ["-fexceptions"],
"xcode_settings": {
"GCC_ENABLE_CPP_EXCEPTIONS": "YES"
}
}
]
}
Node.js での使用:
const gpu = require('./build/Release/gpu_buffer.node')
const buffer = gpu.allocateGPUBuffer(1024 * 1024 * 4) // 4MB GPU メモリ
node-ffi による動的ライブラリ呼び出し(簡易版)
ネイティブコンパイルなしで C ライブラリを呼び出す場合:
import Library from 'ffi-napi'
import ref from 'ref-napi'
const libCuda = new Library('libcuda.so.1', {
cuInit: ['int', ['int']],
cuDeviceGetCount: ['int', [ref.refType(ref.types.int)]],
})
const count = ref.alloc(ref.types.int)
libCuda.cuDeviceGetCount(count)
console.log(`Detected ${count.deref()} CUDA devices`)
セキュアなコンテキストブリッジとサンドボックス設計
サンドボックスの有効化と権限管理
Electron は nodeIntegration を無効化 してセキュリティを強化すべきです:
// main.ts
const win = new BrowserWindow({
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false,
preload: path.join(__dirname, 'preload.ts'),
sandbox: true,
},
})
preload.ts でホワイトリスト化された IPC のみを露出:
// main/preload.ts
import { contextBridge, ipcRenderer } from 'electron'
const allowedChannels = {
// LLM チャネル
'llm:generate': true,
'llm:stream': true,
'model:status': true,
// ファイル操作
'file:open': true,
'file:save': true,
// システム情報
'system:info': true,
}
contextBridge.exposeInMainWorld('electronAPI', {
ipcInvoke: (channel: string, args: any) => {
if (allowedChannels[channel]) {
return ipcRenderer.invoke(channel, args)
}
throw new Error(`IPC channel "${channel}" is not allowed`)
},
ipcOn: (channel: string, callback: (data: any) => void) => {
if (allowedChannels[channel]) {
ipcRenderer.on(channel, (_, data) => callback(data))
}
},
})
// TypeScript 型定義
declare global {
interface Window {
electronAPI: {
ipcInvoke: (channel: string, args: any) => Promise<any>
ipcOn: (channel: string, callback: (data: any) => void) => void
}
}
}
レンダラープロセスでの使用:
// renderer/hooks/useIPC.ts
export function useIPC() {
return {
async invoke(channel: string, args: any) {
return window.electronAPI.ipcInvoke(channel, args)
},
on(channel: string, callback: (data: any) => void) {
window.electronAPI.ipcOn(channel, callback)
},
}
}
ファイルアクセスの制限とサンドボックス突破防止
ユーザーのファイルシステムへのアクセスは、必ず「明示的な許可」を得てから実行:
// main/ipc/file.ts
ipcMain.handle('file:open-dialog', async (event) => {
const { filePaths } = await dialog.showOpenDialog(mainWindow, {
properties: ['openFile'],
filters: [
{ name: 'Text Files', extensions: ['txt', 'md'] },
{ name: 'All Files', extensions: ['*'] },
],
})
return filePaths
})
ipcMain.handle('file:read', async (event, filePath: string) => {
// パス走査攻撃を防止
const normalizedPath = path.normalize(path.resolve(filePath))
const userDir = app.getPath('userData')
if (!normalizedPath.startsWith(userDir)) {
throw new Error('Access denied: file is outside user data directory')
}
return fs.promises.readFile(normalizedPath, 'utf-8')
})
ipcMain.handle('file:save', async (event, filePath: string, content: string) => {
const normalizedPath = path.normalize(path.resolve(filePath))
const userDir = app.getPath('userData')
if (!normalizedPath.startsWith(userDir)) {
throw new Error('Access denied: cannot write outside user data directory')
}
await fs.promises.writeFile(normalizedPath, content, 'utf-8')
})
Content Security Policy(CSP)の設定
// main.ts
const win = new BrowserWindow({
webPreferences: {
contentSecurityPolicy: "default-src 'self'; script-src 'self'",
},
})
自動アップデートシステムの実装
electron-updater による差分更新
Electron アプリの自動更新は electron-updater で実装するのが標準です:
// main/services/auto-updater.ts
import { autoUpdater } from 'electron-updater'
export class AutoUpdater {
constructor(private mainWindow: BrowserWindow) {
this.setupAutoUpdater()
}
private setupAutoUpdater() {
// GitHub Releases からの自動ダウンロード
autoUpdater.checkForUpdatesAndNotify()
autoUpdater.on('update-available', (info) => {
console.log(`New version available: ${info.version}`)
this.mainWindow.webContents.send('update:available', {
version: info.version,
releaseDate: info.releaseDate,
})
})
autoUpdater.on('update-not-available', () => {
console.log('App is up to date')
})
autoUpdater.on('download-progress', (progress) => {
this.mainWindow.webContents.send('update:progress', {
percent: progress.percent,
bytesPerSecond: progress.bytesPerSecond,
})
})
autoUpdater.on('update-downloaded', () => {
this.mainWindow.webContents.send('update:downloaded')
})
autoUpdater.on('error', (error) => {
console.error('Auto-updater error:', error)
this.mainWindow.webContents.send('update:error', {
error: error.message,
})
})
}
async checkForUpdates() {
return autoUpdater.checkForUpdates()
}
async installUpdates() {
autoUpdater.quitAndInstall()
}
}
IPC ハンドラー:
// main/ipc/updater.ts
import { autoUpdater } from 'electron-updater'
ipcMain.handle('updater:check', async () => {
return autoUpdater.checkForUpdates()
})
ipcMain.handle('updater:install', async () => {
autoUpdater.quitAndInstall()
})
レンダラー側の UI:
// renderer/components/UpdatePrompt.tsx
import { useEffect, useState } from 'react'
export function UpdatePrompt() {
const [updateInfo, setUpdateInfo] = useState(null)
const [downloadProgress, setDownloadProgress] = useState(0)
useEffect(() => {
// アップデート利用可能通知
window.electronAPI.ipcOn('update:available', (info) => {
setUpdateInfo(info)
})
// ダウンロード進捗
window.electronAPI.ipcOn('update:progress', (progress) => {
setDownloadProgress(progress.percent)
})
// ダウンロード完了
window.electronAPI.ipcOn('update:downloaded', () => {
setDownloadProgress(100)
})
}, [])
if (!updateInfo) return null
return (
<div className="update-banner">
<h3>新しいバージョンが利用可能です ({updateInfo.version})</h3>
<div className="progress-bar">
<div style={{ width: `${downloadProgress}%` }} />
</div>
<p>{downloadProgress.toFixed(0)}% ダウンロード中...</p>
{downloadProgress === 100 && (
<button onClick={() => window.electronAPI.ipcInvoke('updater:install')}>
再起動して更新
</button>
)}
</div>
)
}
package.json での設定:
{
"build": {
"publish": {
"provider": "github",
"owner": "your-org",
"repo": "your-app"
}
}
}
パッケージングとクロスプラットフォームビルド
electron-builder による配布パッケージ作成
# macOS (DMG + App Store)
npm run build -- --mac dmg mas
# Windows (NSIS + Portable)
npm run build -- --win nsis portable
# Linux (AppImage + deb)
npm run build -- --linux appimage deb
electron-builder.json:
{
"appId": "com.example.electron-ai-app",
"productName": "AI Assistant",
"directories": {
"buildResources": "resources",
"output": "dist"
},
"mac": {
"target": ["dmg", "zip"],
"hardenedRuntime": true,
"entitlements": "resources/entitlements.mac.plist",
"gatekeeper-assess": false
},
"win": {
"target": [
{
"target": "nsis",
"arch": ["x64", "arm64"]
},
{
"target": "portable",
"arch": ["x64"]
}
],
"signingHashAlgorithms": ["sha256"],
"certificateFile": "resources/cert.pfx",
"certificatePassword": "YOUR_PASSWORD"
},
"linux": {
"target": ["AppImage", "deb"],
"category": "Utility"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true
}
}
CI/CD パイプライン(GitHub Actions)
name: Build and Release
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
npm ci
pip install -r python/requirements.txt
- name: Build
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
run: npm run build
- name: Upload artifacts
uses: softprops/action-gh-release@v1
with:
files: dist/*
個人開発者の視点から(実体験メモ)
パフォーマンス最適化とメモリ管理
メモリリークの検出と防止
Electron アプリでよくあるメモリリークの原因と対策:
// ❌ 悪い例:リスナーが登録されたままになる
function setupListener() {
ipcRenderer.on('model:status', (status) => {
updateUI(status)
})
// リスナー削除なし → メモリリーク
}
// ✅ 良い例:コンポーネント アンマウント時にリスナー削除
useEffect(() => {
function handleModelStatus(status) {
updateUI(status)
}
ipcRenderer.on('model:status', handleModelStatus)
return () => {
ipcRenderer.removeListener('model:status', handleModelStatus)
}
}, [])
DevTools でのメモリプロファイリング:
// renderer/hooks/useMemoryMonitor.ts
export function useMemoryMonitor(interval: number = 5000) {
useEffect(() => {
const timer = setInterval(() => {
const usage = process.getProcessMemoryInfo()
console.log(`Memory: ${(usage.heapUsed / 1024 / 1024).toFixed(2)} MB`)
if (usage.heapUsed > 500 * 1024 * 1024) {
console.warn('High memory usage detected!')
}
}, interval)
return () => clearInterval(timer)
}, [interval])
}
AI 推論のバッチ処理と キューイング
複数のリクエストが同時に到着した場合、キューイング でメモリ効率を改善:
// main/services/inference-queue.ts
class InferenceQueue {
private queue: QueueItem[] = []
private processing = false
private concurrency = 1
async enqueue(prompt: string, options: GenerateOptions) {
return new Promise((resolve, reject) => {
this.queue.push({ prompt, options, resolve, reject })
this.processQueue()
})
}
private async processQueue() {
if (this.processing || this.queue.length === 0) {
return
}
this.processing = true
while (this.queue.length > 0) {
const item = this.queue.shift()!
try {
const result = await this.llmService.generate(
item.prompt,
item.options
)
item.resolve(result)
} catch (error) {
item.reject(error)
}
}
this.processing = false
}
}
V8 スナップショットによるバイナリキャッシング
Electron のスタートアップ高速化(参考:Cursor、Windsurf):
// electron.vms.config.js
module.exports = {
snapshotCompiler: true,
snapshotImage: 'snapshot.bin',
// preload スクリプトもスナップショットに含める
v8Code: 'src/main/preload.ts',
}