🐫

ブラウザストレージ(localStorageとIndexedDB)のパフォーマンスを比較する

2023/12/06に公開

はじめに

ブラウザウィンドウ間でデータ共有を実装しようとすると、ブラウザストレージを使うという手段があります
今回は比較的扱いやすいlocalStorageとIndexedDBを取り上げて、CRUD(Create/Read/Update/Delete)のインターフェースと簡単なパフォーマンス計測を比較しようと思います

https://developer.mozilla.org/ja/docs/Web/API/Window/localStorage
https://developer.mozilla.org/ja/docs/Web/API/IndexedDB_API

CRUD

データ保存、取得、削除の方法をlocalStorageとIndexedDBでそれぞれ見ていきます

localStorageのCRUD

ここで紹介するまでもないですが以下のような感じになります
セッター、ゲッターなどのメソッドはいくつかありますが一例を紹介します

// Create/Update
localStorage.setItem('id', 1);

// Read
localStorage.getItem('id');

// Delete
localStorage.removeItem('id');

プリミティブではない値(ArrayやObject)を扱う場合は、
localStorageに保存するときJSON.stringifyでJSON→string
取り出す時に文字列で保存されているのでJSON.parseでstring→JSONする必要があります

const json = { id: 1, name: "kthatoto" };
const stringJson = JSON.stringify(json);
//=> '{"id":1,"name":"kthatoto"}'
localStorage.setItem('profile', stringJson);
const stringJson = localStorage.getItem('profile');
//=> '{"id":1,"name":"kthatoto"}'
const json = JSON.parse(stringJson);
//=> { id: 1, name: "kthatoto" }

IndexedDBのCRUD

今回はDexie.jsを使います
IndexedDBを簡単に扱えるライブラリです

まずはDBのセットアップをします

database.ts
import Dexie, { Table } from "dexie";

interface Profile {
  name: string;
  email: string;
  content: string;
}

export class Database extends Dexie {
  simpleTable!: Table<{ id: number, value: number }>;
  profiles!: Table<{ id: number, profile: Profile }>;

  constructor() {
    super('BrowserStorageComparison');
    this.version(1).stores({
      simpleTable: 'id',
      profiles: 'id',
    });
  }
}

export const db = new Database();

DBにアクセスしたいファイルから、上記でexportしたものをimportして各種メソッドを呼び出すことで使えます
基本的に非同期メソッドなので注意が必要です

index.ts
import { db } from "./database";

// Create
await db.simpleTable.add({ id: 1, value: 1 });

// Update
await db.simpleTable.put({ id: 1, value: 1 });

// Read
await db.simpleTable.get(1);

// Delete
await db.simpleTable.delete(1);

CRUDの比較

IndexedDB(Dexie)は初期設定こそ少し手間ですがそのあとはlocalStorageと比べても同じような手軽さでデータの操作ができます
(StringとJSONの変換をしなくていい分こちらの方が楽まである)
また上記のようにDatabaseクラスを定義することで型が効くのでこの点においてはlocalStorageに対する明確なアドバンテージといえます

パフォーマンス計測

1000回読み書きする時間を計測します

localStorageの計測

const startTime = performance.now();
for (let i = 0; i < 1000; i++) {
  const profile = {
    id: i,
    profile: {
      name: "kthatoto",
      email: "kthatoto@example.com",
      content: "ああああああああああああああ",
    },
  };
  localStorage.setItem(`id_${i}`, JSON.stringify(profile));
  JSON.parse(localStorage.getItem(`id_${i}`)!);
}
const endTime = performance.now();
const time = endTime - startTime;
console.log(`${time}ms`);

localStorageの計測結果

10回計測の平均を出します

9ms
9.799999952316284ms
10.099999904632568ms
10.700000047683716ms
10.700000047683716ms
10.900000095367432ms
10.800000190734863ms
7.8999998569488525ms
7ms
8.200000047683716ms

AVG: 9.510000014305115ms

IndexedDBの計測

const startTime = performance.now();
for (let i = 0; i < 1000; i++) {
  const profile = {
    id: i,
    profile: {
      name: "kthatoto",
      email: "kthatoto@example.com",
      content: "ああああああああああああああ",
    },
  };
  await db.profiles.put(profile);
  await db.profiles.get(i);
}
const endTime = performance.now();
const time = endTime - startTime;
console.log(`${time}ms`);

IndexedDBの計測結果

3126.2999999523163ms
3042.699999809265ms
3133.399999856949ms
3082ms
3052.2000000476837ms
2898.9000000953674ms
2954.2000000476837ms
3044.2000000476837ms
3048.0999999046326ms
3014.9000000953674ms

AVG: 3039.689999985695ms

パフォーマンス計測結果の比較

約300倍の違いがありました

localStorage IndexedDB
9.51ms 3039.69ms

localStorageの読み書きが圧倒的に速いという結果になりました

IndexedDBの使いどころ

パフォーマンス計測結果だけ見てみるとlocalStorage一択ということになりそうですが、
以下のような状況のときはIndexedDBを選択する余地はあります

  • 単純に扱いたいデータ容量がlocalStorageの最大容量を超える時
  • 型が効くので複雑な構造をしたデータを扱いたいとき
  • DBインデックスによる高速検索を活用したい時

おわりに

IndexedDBを色々使っていてlocalStorageよりパフォーマンスが良いと感じたので実際に計測してみようという動機で始めましたが予想と全く逆の結果となってしまいました
おそらく1つのDBテーブルに当たるものをlocalStorageで1つのキーで配列として管理していたことにより同時読み書きがうまくいかなかったことによるものな気がしています
localStorageで items というキーにitemsテーブルに相当するものを全て入れるのではなく item_1 item_2 ...のようにキーを分けて管理することでlocalStorageをうまく扱えるようになるかもしれないですが検証はまたいつか

Discussion