IndexedDB の使用に関するベスト プラクティス

IndexedDB と一般的な状態管理ライブラリの間でアプリケーションの状態を同期するためのベスト プラクティスについて学習します。

Philip Walton 氏
Philip Walton (Philip Walton)

ユーザーが初めてウェブサイトまたはアプリを読み込むとき、多くの場合、UI のレンダリングに使用する初期アプリの状態の構築にはかなりの作業が伴います。たとえば、アプリでユーザーをクライアントサイドで認証し、ページに表示するデータをすべて取得する前に、複数の API リクエストを行う必要が生じることがあります。

アプリケーションの状態を IndexedDB に保存すると、再アクセスの読み込み時間を短縮できます。その後、アプリはバックグラウンドで任意の API サービスと同期し、stale-while-revalidate 戦略により、UI を遅延的に新しいデータで更新できます。

IndexedDB のもう 1 つの便利な使い方は、ユーザー作成コンテンツを、サーバーにアップロードする前に一時ストアとして保存することも、リモートデータのクライアントサイド キャッシュとして保存することもできます。もちろん、その両方としても有効です。

ただし、IndexedDB を使用する際には、API を初めて使用するデベロッパーにはすぐにはわからない重要な考慮事項が多数あります。この記事では、一般的な疑問に答え、IndexedDB でデータを永続化する際に注意すべき最も重要な事項について説明します。

アプリの予測可能性を維持する

IndexedDB に関する複雑さの多くは、デベロッパー(開発者)が制御できない要因があまりにも多いという事実に起因しています。このセクションでは、IndexedDB の使用時に留意する必要がある問題の多くについて説明します。

すべてのプラットフォームの IndexedDB にすべてを保存できるわけではない

ユーザーが生成した画像や動画など、サイズの大きいファイルを保存する場合は、File オブジェクトや Blob オブジェクトとして保存してみてください。これは一部のプラットフォームでは機能しますが、他のプラットフォームでは機能しません。特に、iOS 版 Safari では Blob を IndexedDB に保存できません。

幸いなことに、BlobArrayBuffer に変換することはそれほど難しくなく、その逆も同様です。IndexedDB への ArrayBuffer の保存は非常にサポートされています。

ただし、Blob には MIME タイプがありますが、ArrayBuffer にはありません。変換を正しく行うには、型をバッファとともに保存する必要があります。

ArrayBufferBlob に変換するには、Blob コンストラクタを使用します。

function arrayBufferToBlob(buffer, type) {
  return new Blob([buffer], { type: type });
}

もう一方はやや複雑で、非同期プロセスです。FileReader オブジェクトを使用すると、blob を ArrayBuffer として読み取ることができます。読み取りが完了すると、読み取りで loadend イベントがトリガーされます。このプロセスは、次のように Promise でラップできます。

function blobToArrayBuffer(blob) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.addEventListener('loadend', () => {
      resolve(reader.result);
    });
    reader.addEventListener('error', reject);
    reader.readAsArrayBuffer(blob);
  });
}

ストレージへの書き込みが失敗することがある

IndexedDB への書き込み時のエラーは、さまざまな理由で発生する可能性があります。場合によっては、デベロッパーが制御できない理由も考えられます。たとえば、一部のブラウザは現在、シークレット ブラウジング モードのときの IndexedDB への書き込みが許可されていません。また、ユーザーが使用しているデバイスのディスク容量がほぼ不足し、ブラウザが何も保存できないという可能性もあります。

このため、IndexedDB コードには適切なエラー処理を常に実装することが非常に重要です。また、アプリの状態を(保存だけでなく)メモリに保持することをおすすめします。これにより、シークレット ブラウジング モードで実行している場合や、保存容量が利用できない場合でも(ストレージを必要とする他のアプリ機能が動作しない場合でも)UI が壊れません。

IndexedDB オペレーションのエラーをキャッチするには、IDBDatabaseIDBTransaction、または IDBRequest オブジェクトを作成する際に error イベントのイベント ハンドラを追加します。

const request = db.open('example-db', 1);
request.addEventListener('error', (event) => {
  console.log('Request error:', request.error);
};

保存データは、ユーザーによって変更または削除されている可能性があります

不正アクセスを制限できるサーバーサイド データベースとは異なり、クライアントサイド データベースはブラウザ拡張機能とデベロッパー ツールからアクセスでき、ユーザーがクリアできます。

ローカルに保存されているデータをユーザーが変更することはめったにありませんが、ユーザーがそのデータを消去することはよくあります。アプリケーションがこれらの両方のケースをエラーなしで処理できることが重要です。

保存されているデータが古くなっている可能性があります

前のセクションと同様に、ユーザーが自分でデータを変更していなくても、ストレージに保存されているデータが古いバージョンのコードによって書き込まれた可能性もあります。おそらく、バグのあるバージョンも書き込まれている可能性があります。

IndexedDB には、スキーマ バージョンのサポートと、IDBOpenDBRequest.onupgradeneeded() メソッドによるアップグレードが組み込まれています。ただし、以前のバージョンから移行するユーザーを処理できるようにアップグレード コードを記述する必要があります(バグのあるバージョンを含む)。

考えられるすべてのアップグレード パスとケースを手動でテストすることは難しい場合が多いため、単体テストはとても便利です。

アプリのパフォーマンスを維持する

IndexedDB の重要な機能の一つに非同期 API がありますが、それを使用するときはパフォーマンスを気にする必要がないと思い込んではいけません。それでも不適切な使用によってメインスレッドがブロックされ、ジャンクや応答不能になるケースはいくつかあります。

原則として、IndexedDB への読み取りと書き込みは、アクセス対象のデータに必要なサイズを超えないようにしてください。

IndexedDB を使用すると、大規模なネストされたオブジェクトを 1 つのレコードとして保存できます(これは、明らかにデベロッパーの観点からは非常に便利です)。ただし、この手法は避けるべきです。これは、IndexedDB がオブジェクトを格納するときに、まずそのオブジェクトの構造化クローンを作成し、構造化クローン作成プロセスがメインスレッドで行われるためです。オブジェクトが大きいほど、ブロック時間は長くなります。

一般的な状態管理ライブラリ(Redux など)のほとんどは、状態ツリー全体を 1 つの JavaScript オブジェクトとして管理しているため、アプリケーションの状態を IndexedDB に保持する方法を計画する際にいくつかの課題が生じます。

この方法で状態を管理することには多くの利点があります(コードの推論とデバッグが容易になるなど)。状態ツリー全体を IndexedDB に 1 つのレコードとして保存するだけでは魅力的で便利ですが、(スロットリング/デバウンスがあったとしても)変更のたびにこれを行うと、メインスレッドが不必要にブロックされ、ブラウザがクラッシュしたり、タブがクラッシュしたりする可能性があります。

状態ツリー全体を 1 つのレコードに格納するのではなく、個々のレコードに分割して、実際に変更されるレコードのみを更新する必要があります。

画像、音楽、動画などのサイズの大きいアイテムを IndexedDB に保存する場合についても同様です。大きなオブジェクト内ではなく独自のキーで各アイテムを保存することで、バイナリ ファイルの取得にかかる費用を支払うことなく、構造化データを取得できます。

他のベスト プラクティスと同様に、これはオールオアナッシングのルールではありません。状態オブジェクトを分割して最小限の変更セットを書き込むだけでは不可能な場合は、データをサブツリーに分割して書き込みのみを行い、常に状態ツリー全体を書き込むことをおすすめします。少しの改善でも、まったく改善しないよりはましです。

最後に、記述したコードがパフォーマンスに及ぼす影響を常に測定する必要があります。IndexedDB への小規模な書き込みの方が大規模な書き込みよりもパフォーマンスが向上することは確かですが、これは、アプリケーションが行っている IndexedDB への書き込みが実際にメインスレッドをブロックし、ユーザー エクスペリエンスが低下する長いタスクにつながる場合にのみ重要です。最適化の対象を理解するには 測定が重要です

まとめ

デベロッパーは IndexedDB などのクライアント ストレージ メカニズムを活用して、セッション間で状態を維持するだけでなく、再アクセス時に初期状態を読み込む時間を短縮することで、アプリケーションのユーザー エクスペリエンスを向上させることができます。

IndexedDB を適切に使用するとユーザー エクスペリエンスは大幅に向上しますが、使い方を間違えた場合や、エラーケースを処理できないと、アプリが壊れてユーザーの不満につながる可能性があります。

クライアント ストレージには、制御できない多くの要因が関わるため、コードを十分にテストし、最初は発生する可能性の低いエラーであっても、適切に処理することが重要です。