LocalStorage / IndexedDB を Tauri アプリで使う

プラグインなしで使える localStorage と IndexedDB の使い分け、保存場所、消えるリスクと容量、Store や SQL プラグインへ移る判断基準をまとめる。

データ保存 対象: Tauri 2.x 更新日: 読了目安: 約5分 db-007
目次
  1. 前提条件
  2. 1. フロントエンドから実装する (TypeScript)
  3. localStorage: 文字列のキーバリュー
  4. IndexedDB: 構造化データを非同期で
  5. 使い分けの目安
  6. 2. バックエンドから実装する (Rust)
  7. 動作確認
  8. よくあるエラーと対処法
  9. OS ごとの違いと注意点
  10. 関連レシピ

Tauri のフロントエンドは普通の WebView なので、localStorage も IndexedDB もそのまま動きます。プラグインも権限設定も不要で、UI の一時状態や小さなキャッシュには最も手軽です。一方で実体は WebView が管理するフォルダに書かれ、消えるタイミングもアプリの都合とは無関係です。2 つの API の使い分けと、どこから tauri-plugin-store や SQL プラグインへ移すべきかを整理します。

前提条件

プラグインは不要です。IndexedDB を Promise で扱うために idb を入れます。

npm install idb

1. フロントエンドから実装する (TypeScript)

localStorage: 文字列のキーバリュー

// 値は文字列のみ。オブジェクトは JSON にする
localStorage.setItem('ui.theme', 'dark');
localStorage.setItem('ui.sidebar', JSON.stringify({ open: true, width: 240 }));

const theme = localStorage.getItem('ui.theme') ?? 'light'; // 無ければ null
const sidebar = JSON.parse(localStorage.getItem('ui.sidebar') ?? '{"open":true,"width":200}');

localStorage.removeItem('ui.theme');

API は同期で、呼び出しの間 UI スレッドが止まります。数百 KB の JSON を起動時に読むと初回描画が遅れます。

IndexedDB: 構造化データを非同期で

import { openDB, DBSchema } from 'idb';

interface NotesDB extends DBSchema {
  notes: {
    key: number;
    value: { id?: number; title: string; body: string; updatedAt: number };
    indexes: { 'by-updated': number };
  };
}

const dbPromise = openDB<NotesDB>('notes-db', 1, {
  upgrade(db) {
    // 初回作成時と version を上げたときだけ呼ばれる
    const store = db.createObjectStore('notes', { keyPath: 'id', autoIncrement: true });
    store.createIndex('by-updated', 'updatedAt');
  },
});

export async function saveNote(title: string, body: string) {
  return (await dbPromise).put('notes', { title, body, updatedAt: Date.now() }); // 戻り値は id
}

export async function recentNotes(limit = 20) {
  const all = await (await dbPromise).getAllFromIndex('notes', 'by-updated');
  return all.reverse().slice(0, limit);
}

Uint8Array や Blob もそのまま保存できるので、サムネイルのキャッシュにも使えます。

使い分けの目安

観点localStorageIndexedDB
値の型文字列のみオブジェクト、バイナリ、Blob
容量数 MB(WebView2 は 10MB 程度、WebKit は 5MB 程度)数百 MB 以上(ディスク残量に依存)
API同期非同期(トランザクション)
向く用途テーマ、最後に開いたタブ、UI 状態一覧のキャッシュ、オフライン下書き、サムネイル

2. バックエンドから実装する (Rust)

どちらも WebView 内部のストレージなので、Rust 側から読み書きする API はありません。Rust からも扱いたいデータは最初から Store プラグインや SQL プラグインに置きます。

動作確認

npm run tauri dev で値を保存し、アプリを閉じて再起動しても残っていれば成功です。実体は次の場所にあります(<identifier> は tauri.conf.json の identifier)。

  • Windows: %LOCALAPPDATA%\<identifier>\EBWebView\Default\ 配下の Local Storage\leveldb と IndexedDB
  • macOS: ~/Library/WebKit/<identifier>/WebsiteData/ 配下(開発中は実行ファイル名で分かれることがあります)
  • Linux: ~/.local/share/<identifier>/ 配下に WebKitGTK が localstorage / databases を作ります

よくあるエラーと対処法

  • 本番ビルドにすると開発中のデータが見えない: 保存先はオリジン単位です。開発時は http://localhost:1420 などの devServer、本番は tauri://localhost(Windows は http://tauri.localhost)と別オリジンになるため、別領域に保存されます。バグではありません。
  • QuotaExceededError: localStorage の上限超過です。IndexedDB へ移すか、Store プラグインでファイルに書きます。
  • openDB が VersionError という趣旨のエラー: 既存 DB より小さい version で開いています。version は下げられないので、数値を戻すか DB 名を変えます。
  • upgrade が呼ばれず blocked が発火する: 別ウィンドウが同じ DB を古い version で開いたままです。blocking コールバックで接続を閉じるか、DB を開くウィンドウを 1 つに絞ります。

OS ごとの違いと注意点

  • アンインストールで消える: Tauri の NSIS アンインストーラーには「アプリデータを削除する」チェックがあり、選ばれると %LOCALAPPDATA%\<identifier> ごと消えます。macOS で .app をゴミ箱に入れた場合は残りますが、クリーナー系ツールが ~/Library/WebKit を掃除することがあります。
  • WebView の更新・リセットで消える: WebView2 ランタイムや WebKitGTK の更新で内部形式が変わると、まれに読めなくなります。ユーザーが「サイトデータの削除」に相当する操作をできるかも OS 次第です。
  • 移行の判断基準: 「消えたら困る」「Rust からも読みたい」「複数ウィンドウで共有したい」「ファイルとしてユーザーに渡したい」のどれかに当てはまれば Store プラグイン へ。「検索・並べ替え・集計がある」「数万件以上ある」なら SQLite へ移します。ブラウザストレージは「消えても再生成できるもの」に限定するのが安全です。

関連レシピ

参考リンク(公式ドキュメント)

Web Ninja

この記事を書いた人

Web Ninja ウェブエンジニア (Web Engineer)

会社員ネットワークエンジニアから独立してかれこれ 25 年以上 Web エンジニアとして活動中。普段は JavaScript と Node.js を自在に操り、時には C++ や Perl といった古流の技も嗜みます。近年は Tauri × Rust という新たな武器を手に、デスクトップアプリ開発の最前線を駆け抜けています。「作りたい」を「作れる」に変えるための、実践的な「技」をお届けします。

お問い合わせ: tauri.ninja@gmail.com

内容の誤り・動かないコードを報告する