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

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

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

アプリケーションの状態を IndexedDB に保存すると、再訪問時の読み込み時間を短縮できます。その後アプリは、古い再検証戦略を採用して、バックグラウンドで API サービスと同期し、新しいデータで UI を遅れて更新できます。

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

ただし、IndexedDB を使用する場合、API を初めて使用するデベロッパーにとっては、すぐにはわかりかねる重要な考慮事項が多くあります。この記事では、よくある質問への回答と、IndexedDB でデータを永続化する際に留意すべき最も重要な点について説明します。

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

IndexedDB が複雑になる主な原因は、デベロッパーが制御できない要因が非常に多くあることです。このセクションでは、IndexedDB を使用する際に留意すべき多くの問題について説明します。

プラットフォームによっては IndexedDB に保存できないデータがある

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

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

ただし、Blob には MIME タイプがありますが、ArrayBuffer には MIME タイプがないことに注意してください。変換を正しく行うには、型をバッファと一緒に保存する必要があります。

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 オペレーションのエラーをキャッチするには、IDBDatabaseIDBTransactionIDBRequest の各オブジェクトを作成するたびに 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 など)のほとんどは、状態ツリー全体を単一の JavaScript オブジェクトとして管理することで機能するため、アプリケーションの状態を IndexedDB に維持する方法を計画する際には、いくつかの課題が生じます。

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

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

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

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

最後に、作成するコードのパフォーマンスへの影響を測定する必要があります。IndexedDB への小規模な書き込みの方が大規模な書き込みよりも優れたパフォーマンスを発揮するのは事実ですが、この点が問題になるのは、アプリケーションが行っている IndexedDB への書き込みが、メインスレッドをブロックしてユーザー エクスペリエンスを低下させる時間のかかるタスクを実際に実行する場合に限られます。測定は重要です 何を重視して最適化の目標かを把握することが重要です

まとめ

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

IndexedDB を適切に使用するとユーザー エクスペリエンスは劇的に向上しますが、適切に使用しない場合やエラーケースの処理に失敗すると、アプリが壊れてユーザーの不満につながる可能性があります。

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