ブラウザ内ストレージの選び方|IndexedDB・OPFS・SQLite WASM・DuckDB-Wasm比較

ブラウザの保存先、どう選ぶ? IndexedDB・OPFS・SQLite・DuckDBの選び方を明るい背景に示したアイキャッチ ブラウザDB・ストレージ
カテゴリー
ブラウザDB・ストレージ
公開日
2026.09.22

はじめに

Webアプリのデータをブラウザへ保存する方法はlocalStorageだけではありません。

オブジェクトを検索したい、SQLiteを動かしたい、数百MBのファイルを扱いたい、ParquetをSQL集計したいなど、目的によって適切な技術が変わります。

この記事では次の5種類を比較します。

  • localStorage
  • IndexedDB
  • OPFS
  • SQLite WASM+OPFS
  • DuckDB-Wasm

「どれが最も高性能か」ではなく、データ構造、読み書き、分析、寿命、対応環境から選ぶためのガイドです。

ブラウザにデータを保存する方法を初めて選ぶ方向けです。比較を読むだけなら実行環境は不要です。後半の機能確認ページを試す場合は、HTMLファイルの作成とターミナルの基本操作を使います。

ここで比べるものはすべて同じ階層の技術ではありません。localStorage・IndexedDB・OPFSはブラウザが提供する保存API、SQLite WASM・DuckDB-Wasmは追加で読み込むデータベースエンジンです。保存場所と処理エンジンは組み合わせて使えます。

先に結論

主な要件最初に検討する候補
小さな設定値を数個保存localStorageまたはIndexedDB
JavaScriptオブジェクトをキーで検索IndexedDB
ファイルや大きなバイナリを保存OPFS
リレーショナルデータをSQLで更新SQLite WASM+OPFS
CSV・Parquetを集計分析DuckDB-Wasm
複数端末・複数利用者で共有サーバーDBと同期層を検討

ブラウザ内ストレージだけでは、別端末との自動同期やバックアップは実現しません。

localStorage

localStorageは文字列のキーと値を保存する同期APIです。

localStorage.setItem("theme", "dark");
const theme = localStorage.getItem("theme");

向いている用途

  • テーマや表示モード
  • 小さな設定値
  • 複雑な検索が不要な短い文字列

注意点

  • 同期APIなのでメインスレッドを止める
  • 文字列以外は自分で変換する
  • 大きなデータには向かない
  • トランザクションや複雑な検索がない

TODO一覧全体を巨大なJSONとして毎回読み書きする設計は、データが増えたときに扱いにくくなります。

IndexedDB

IndexedDBは、キーとオブジェクトを非同期で保存できるブラウザ標準データベースです。Object Store、インデックス、トランザクションを利用できます。

向いている用途

  • JavaScriptオブジェクト
  • オフラインWebアプリ
  • キーやインデックスを使った検索
  • Service Workerと組み合わせるPWA

注意点

  • Promiseラッパーなしの生APIは記述量が多い
  • SQLではない
  • JOINや分析クエリをそのまま書けない
  • スキーマ更新時にバージョンアップ処理が必要

ブラウザ標準だけで構成でき、追加のWASMエンジンをダウンロードしなくてよい点は大きな利点です。

OPFS

OPFSはオリジン専用の非公開ファイルシステムです。

向いている用途

  • ファイルやディレクトリとして管理したいデータ
  • 動画、画像、モデル、編集途中のバイナリ
  • WebAssembly製アプリの仮想ファイル
  • SQLiteのDBファイル

注意点

  • データベース検索機能は提供しない
  • 通常のファイル管理画面から直接見えない
  • サイトデータ削除の影響を受ける
  • 同期アクセスはDedicated Worker内に限定される

OPFSは保存場所です。SQL、インデックス、スキーマ、競合解決を自動的に提供するわけではありません。

SQLite WASM+OPFS

SQLiteエンジンをWebAssemblyで動かし、DBファイルをOPFSへ保存する構成です。

向いている用途

  • テーブルと外部キーを使うデータ
  • SQLによる検索と更新
  • 既存SQLite設計の再利用
  • Local-firstアプリ

注意点

  • WASMとSQLite初期化の費用がある
  • VFSごとにヘッダーや並行接続の条件が違う
  • DB移行、ロック、SQLITE_BUSYを扱う必要がある
  • サーバー上の共有SQLiteにはならない

リレーショナルデータには便利ですが、単純な設定値1つのために導入する必要はありません。

DuckDB-Wasm

DuckDB-Wasmは分析向けDuckDBをブラウザで動かす構成です。

向いている用途

  • CSV、JSON、Parquetの集計
  • 列指向データ分析
  • 複雑な集約、JOIN、Window関数
  • データ可視化ツール

注意点

  • CRUD中心のアプリ向けではない
  • WASMバンドルが大きい
  • メモリ上限の影響を受ける
  • ブラウザ版ではネイティブ版と同じ機能を無条件に期待できない
  • 標準構成では永続化を別途検討する

詳細比較

項目localStorageIndexedDBOPFSSQLite WASMDuckDB-Wasm
ブラウザ標準はいはいはいいいえいいえ
主な形式文字列オブジェクトファイルリレーショナルDB分析DB
SQLなしなしなしありあり
非同期いいえはいはいWorker構成によるはい
インデックスなしありなしありあり
ファイル保存不向きBlob可得意DBファイル読み込み中心
更新中心小規模のみ得意自前実装得意主目的ではない
集計分析不向き自前実装自前実装可能得意
追加ダウンロードなしなしなしSQLite WASMDuckDB-Wasm

選択手順

1. データを共有する必要があるか

別端末、複数利用者、管理画面から同じデータを参照する場合、ブラウザ内保存だけでは足りません。サーバーDBを正本にするか、同期プロトコルを設計します。

2. データはファイルかレコードか

  • 動画、モデル、編集ファイル:OPFS
  • オブジェクトの集合:IndexedDB
  • 表と関係:SQLite
  • 分析対象ファイル:DuckDB

3. 読み取りと書き込みの比率を確認する

頻繁な小規模更新と、大量データを一括集計する処理では適切なDBが異なります。

4. 初回ダウンロードを許容できるか

SQLite WASMやDuckDB-Wasmはエンジン本体を取得します。小さな設定画面では、エンジン取得時間の方が機能に対して大きすぎる場合があります。

5. データ消失時の影響を決める

どのブラウザストレージも、利用者によるサイトデータ削除の影響を受けます。失ってはいけないデータなら、エクスポート、同期、バックアップを用意します。

機能検出ページ

利用中のブラウザでAPIの入口を確認するコードです。SQLiteやDuckDBの導入状況や処理速度を測るものではありません。

Node.js 22.12以上(この例では24系)とnpmを用意し、作業フォルダーを作ります。

mkdir browser-storage-check
cd browser-storage-check
npm init -y
npm install --save-dev vite@7.3.6

次をindex.htmlとして保存します。

<!doctype html>
<html lang="ja">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ブラウザストレージ機能確認</title>
<style>
  body { max-width: 720px; margin: 24px auto; padding: 0 16px; font-family: system-ui; }
  pre { white-space: pre-wrap; overflow-wrap: anywhere; }
</style>
<h1>ブラウザストレージ機能確認</h1>
<button id="check">ストレージ機能を確認</button>
<pre id="output" role="status"></pre>
<script type="module">
  document.querySelector("#check").addEventListener("click", async () => {
    const errors = {};
    async function checkStorageMethod(name) {
      try {
        return typeof navigator.storage?.[name] === "function"
          ? await navigator.storage[name]()
          : null;
      } catch (error) {
        errors[name] = error.name;
        return null;
      }
    }

    const result = {
      localStorage: (() => {
        try {
          let key;
          do {
            key = `__storage_test_${crypto.randomUUID()}`;
          } while (localStorage.getItem(key) !== null);
          localStorage.setItem(key, "1");
          const readable = localStorage.getItem(key) === "1";
          localStorage.removeItem(key);
          return readable;
        } catch (error) {
          errors.localStorage = error.name;
          return false;
        }
      })(),
      indexedDB: "indexedDB" in globalThis,
      opfs: Boolean(navigator.storage?.getDirectory),
      storageEstimate: await checkStorageMethod("estimate"),
      persisted: await checkStorageMethod("persisted"),
      errors,
    };

    document.querySelector("#output").textContent = JSON.stringify(result, null, 2);
  });
</script>
</html>

npx viteを実行し、表示されたローカルURL(通常はhttp://localhost:5173)を開いてボタンを押します。終了はターミナルでCtrl+Cです。HTMLの直接起動ではなく、localhostまたはHTTPSのページで試してください。

結果の読み方

  • localStorage: true:ランダムなテスト用キーの書き込み・読み取り・削除ができました。既存データのキーは使いません。
  • indexedDB: trueopfs: true:APIの入口が存在します。実際にファイルやDBを作れたという意味ではありません。
  • storageEstimateusagequotaがバイト単位で表示されます。推定値であり、空き容量や今後の保存成功を保証しません。
  • persisted: false:永続ストレージとして保護されていない状態です。一般的な結果で、確認コードの失敗ではありません。このページは永続化の許可を要求しません。
  • null:対応するメソッドがないか、呼び出しに失敗しました。errorsにエラー名があれば、その機能だけ確認に失敗しています。

プロパティが存在するだけでは、書き込み成功や十分な容量を保証しません。OPFSやIndexedDBも採用時には実書き込みを確認します。

うまく動かない場合

ボタンが反応しない場合は、ファイル名とコードの保存漏れ、ブラウザのコンソールを確認します。falseやエラーが出る場合は、localhost/HTTPSで開いているか、サイトデータを制限する設定がないか確認してください。プライベートブラウズやブラウザのポリシーでも挙動が変わるため、機能があるだけで保存データが残るとは判断しないでください。

ベンチマークで陥りやすい問題

異なるストレージへ同じコードをそのまま当てても、公平な比較にならない場合があります。

1件ずつ確定しない

SQLiteではトランザクション、IndexedDBでは1トランザクション内の複数操作を利用します。現実的な利用方法で比べます。

シリアライズ費用を含めるか決める

localStorageへオブジェクトを保存するにはJSON変換が必要です。変換時間を含める測定と、ストレージ操作だけの測定を分けます。

初期化と定常処理を分ける

WASMコンパイル、DBオープン、スキーマ作成、初回キャッシュは別に記録します。

結果の正しさを確認する

速度だけでなく、保存件数、順序、型、Unicode、トランザクション失敗後の状態を検証します。

組み合わせる例

1つへ統一する必要はありません。

例えばブラウザ内データ分析アプリなら次のように分けられます。

IndexedDB: 画面設定、最近開いたファイル
OPFS:      大きな元データとキャッシュ
DuckDB:    CSV・Parquetの集計

SQLiteアプリでも、DB本体をOPFS、UI設定をIndexedDBへ置く構成があります。

ただし保存先が増えるほど、削除、移行、バックアップ、容量表示が複雑になります。役割を文書化してください。

セキュリティ上の注意

ブラウザ内保存は、保存データの内容を自動的に暗号化してアプリから守る機能ではありません。同じオリジンで実行されるスクリプトは、そのオリジンの保存領域へアクセスできる可能性があります。

XSS対策、Content Security Policy、外部スクリプト管理、ログアウト時の削除、共有端末での扱いを検討します。

機密情報をlocalStorageへ保存しないという個別ルールだけでなく、オリジン内で動くコード全体を守る必要があります。

関連記事

まとめ

ブラウザ内ストレージは、名前の知名度ではなく、保存するデータと操作方法から選びます。

  • 小さな設定はlocalStorageまたはIndexedDB
  • JavaScriptオブジェクトはIndexedDB
  • ファイルはOPFS
  • 更新中心のSQLデータはSQLite WASM
  • CSV・Parquet分析はDuckDB-Wasm

重要データでは、どれを選んでもエクスポート、同期、バックアップ、削除手順を別途設計してください。

参考リンク

コメント

タイトルとURLをコピーしました