【JavaScript】Web storage 詳説 – ストレージなのに 7 日で消える?

HTML5 という用語が流行りだしたころからブラウザーで利用可能な Web storage ですが、フロントエンドのエンジニアであれば何度も使ったことがあるでしょう。しかしこの Web storage も深堀すると意外な機能や制約があったりします。今回はあまり話題にならない細かい機能も含めてできる限り詳しく解説します。

Web storage のストレージの種類と使いどころ

Web storageには、ローカルストレージとセッションストレージという 2 つのタイプのストレージが用意されています。

ローカルストレージはブラウザーを閉じても残り続け、その後、改めて該当のページにアクセスすると、以前に保存したデータを取り出すことができます。ローカルストレージはオリジンごとに 1 つのストレージスペースが用意されます。したがって、同じサイトを複数のタブで開くと、どのタブからも同じストレージ領域にアクセスすることができます。

一方、セッションストレージはブラウザーを閉じると消えてしまうストレージです。正確に言うと、タブを閉じるとデータが消えてしまいます。ストレージの種類の名前が「セッション」とあるだけに、このストレージはオリジン単位ではなくセッション単位で用意されます。例えば、同じページに複数のタブでアクセスしたとしましょう。この場合、それぞれにストレージが用意され、同じオリジンであるにも関わらず、タブ間でデータは共有されません。

このようにセッションストレージではデータは永続的に保存されないのですが、これは単に JavaScript の変数にデータをセットしたのと変わず、どこで使うのかと疑問に思われるかもしれません。もし一つのページにとどまり、そこでタブを閉じるのであれば、実質的にセッションストレージと JavaScript 変数は同じです。しかし、セッションストレージの本領が発揮されるのは同じオリジンの中でページ遷移した場合です。

ページが遷移すると JavaScript 変数の値は引き継げません。一方でセッションストレージならページを遷移しても値を引き継ぐことができます。このように、ページが遷移してもデータを引き継ぐ必要がありながらも、永続的にデータを保存する必要がない場合にセッションストレージを使います。

Cookie の後継技術?

Web Storage が登場したころ、よく Cookie との違いが紹介されていたのを読んだことがあるのではないでしょうか。中には Web Storage があたかも Cookie の後継技術であるかのように読み取れる記事も見かけました。

もう皆さんはすでに理解されているでしょうが、当然、Web storage は当初から Cookie の後継技術ではありませんし、今なお、利用する目的は全く異なります。

Cookie も Web storage もブラウザー側にデータを保存しておく仕組みという点では同じなのですが、Cookie はサーバーにアクセスするたびに保存データを HTTP リクエストヘッダーを通してサーバーに送り付けます。そのため、ログインセッションの仕組みには Cookie が欠かせません。Cookie は本来はデータ保管庫ではありませんので、容量も 4KB 程とかなり小容量です。

一方で Web storage では、ブラウザー側に保存されたデーターが勝手にサーバーに送信されることはありません。ストレージという名前の通りデータ保管庫として使いますが、その容量は控えめで、オリジンあたり 5 ~ 10 MB 程度と言われています。とはいえ、Cookie と比べたら大容量です。

仮に保存したいデータが 4KB 以下だとしても、アクセスのたびにサーバーに送り付ける必要がないのであれば、Cookie ではなく Web storage を使うべきです。

保存できるのは文字列のみ

Web storage で保存できるデータは文字列のみです。Object オブジェクトや Array オブジェクトはそのまま保存することができません。JSON などの文字列に変換したうえで保存する必要があります。

Web storage の容量が 5 MB、文字列は UTF-16 で保存され、文字列は UTF-16 の 2 バイト文字だけから構成されると仮定すれば、オリジンあたり、おおむね 260 万文字保存することができる計算になります。よほど高度のウェブアプリケーションでない限り、容量は十分と言えるでしょう。

もしバイナリーデータを保存したい、5 MB では足りない、といった場合は、Web storage ではなく、Indexed Database API や File System Access API の Origin Private File System を使うべきでしょう。

Storage オブジェクト

Web storage を利用するためには、まず Storage オブジェクトを取り出す必要があります。

ストレージの種類Storage オブジェクト
ローカルストレージwindow.localStorage
セッションストレージwindow.sessionStorage

ローカルストレージの Storage オブジェクトは window.localStorage から、セッションストレージの Storage オブジェクトは window.sessionStorage から得られます。いずれの Storage オブジェクトも、その使い方は全く同じです。以降、ローカルストレージを前提に説明します。

Storage オブジェクトには、以下のメソッドとプロパティが用意されています。

プロパティ説明
lengthunsigned longストレージに保存されているキー・バリュー・ペアの数を返します。
Storage オブジェクトのプロパティ
メソッド説明
key(n)n 番目に保存されているキー名を返します。保存されているキー・バリュー・ペアの数と同じかそれより大きい値を指定すると null を返します。
getItem(key)指定の key をキー名と関連付けられた値を返します。指定のキー名が存在しなければ null を返します。
setItem(key, value)key をキー名、value をその値として、キー・バリュー・ペアを保存します。
removeItem(key)指定の key をキー名を持つキー・バリュー・ペアを削除します。
clear()すべてのキー・バリュー・ペアを削除します。
Storage オブジェクトのメソッド

次のサンプルコードは、”user”, “time” の 2 つのキーに対して値を保存します。キー名 “user” の値には JSON 文字列をセットします。キー名 “time” の値には整数値となる Date.now() の結果をセットします。そのあと、上記の様々なメソッドとプロパティを使って、データを操作します。

// ローカルストレージの Storage オブジェクト
const storage = window.localStorage;

// ストレージに値を保存する
storage.setItem('user', JSON.stringify({ name: '太郎', age: 34 }));
storage.setItem('time', Date.now());

// ストレージのキー・バリュー・ペアの数を取得
console.log(storage.length); // 2

// キー名 "user" の値を取得して型を表示
const user = storage.getItem('user');
console.log(user); // "{"name":"太郎","age":34}"
console.log(typeof (user)); // "string"

// キー名 "time" の値を取得して型を表示
const time = storage.getItem('time');
console.log(time); // "1661344987887"
console.log(typeof (time)); // "string"

// キー名 "user" を削除
storage.removeItem('user');

// ストレージのキー・バリュー・ペアの数を取得
console.log(storage.length); // 1

// ストレージをクリアする
storage.clear();

// ストレージのキー・バリュー・ペアの数を取得
console.log(storage.length); // 0

上記コードで注目すべきポイントは、Object オブジェクトを setItem() メソッドで保存する場合は JSON.stringify() を使って文字列に変換しておく必要があるところです。逆に getItem() メソッドで値を取り出すと、当然ながら文字列のままです。もし Object オブジェクトにしたいなら JSON.parse() を使って変換してください。

一方、数値を setItem() メソッドで保存する場合は文字列に変換する必要はありません。自動的に文字列に変換されてから保存されます。逆に getItem() メソッドで値を取り出すと、自動的に数値に変換されず文字列のままですので注意してください。

ストレージのキー・バリュー・ペアの一覧を取得するには

前述のサンプルでは初めからキー名が分かっている場合の操作を紹介しましたが、ストレージに保存されているすべてのキー・バリュー・ペアの一覧を取り出したい場合もあるでしょう。その場合は、Storage オブジェクトの key() メソッドを使います。

// ローカルストレージの Storage オブジェクト
const storage = window.localStorage;

// ストレージに値を保存する
storage.setItem('user', JSON.stringify({ name: '太郎', age: 34 }));
storage.setItem('time', Date.now());

// 一覧を取得
for (let i = 0; i < storage.length; i++) {
  const key = storage.key(i);
  const val = storage.getItem(key);
  console.log(key + ': ' + val);
}

このコードを実行すると、次のような結果がコンソールに出力されます。

time: 1661345919946
user: {"name":"太郎","age":34}

Storage オブジェクトを Object オブジェクトのように扱う

Storage オブジェクトは、Object オブジェクトと同じような構文で値へのアクセス方法を提供します。

// ローカルストレージの Storage オブジェクト
const storage = window.localStorage;

// ストレージに値を保存する
storage.user = JSON.stringify({ name: '太郎', age: 34 });
storage.time = Date.now();

// 値を読み取る
console.log(storage.user);
console.log(storage.time);

// キー・バリュー・ペアを削除する
delete storage.user;
delete storage.time;

console.log(storage.length); // 0

本来であれば、値を読み取るなら storage.getItem('user') とするべきところを storage.user と表記することができます。読み取りだけでなく、値をセットする場合にも使えます。また、削除する場合は delete が使えます。

コードの量も減り便利そうに見える構文ですが、個人的な好みを言うと、私はこれを使うことはありません。理由は第三者がコードを見た際に誤解を招く恐れがあるからです。第三者でなくても、しばらく経ってから自分が見たとしても誤解しそうです。

何を誤解するのかというと、例えばソースコードの途中である

delete storage.user;

という文をいきなり見た場合、変数 storage が Web storage の Storage オブジェクトであることに気付かない可能性があります。もちろん、前述の簡単なコードなら見通しも良いため勘違いすることはないでしょうが、もう少し複雑なコードであれば気づかない可能性は否めません(あくまでも私の場合です)。

もしこの簡略構文を使う場合は、見通しの良い短い関数でストレージの操作が完結できる場合に限定した方が良いかもしれません。または、コード量が減るというメリットはなくなりますが、Storage オブジェクトを変数に入れず、都度、window.localStorage と表記するのも良いかもしれません。

// ストレージに値を保存する
window.localStorage.user = JSON.stringify({ name: '太郎', age: 34 });
window.localStorage.time = Date.now();

// 値を読み取る
console.log(window.localStorage.user);
console.log(window.localStorage.time);

// キー・バリュー・ペアを削除する
delete window.localStorage.user;
delete window.localStorage.time;

console.log(window.localStorage.length); // 0

値の更新のイベントをキャッチ

ローカルストレージはオリジンごとにストレージ領域を共有するのは前述の通りです。もし同じオリジンのページを 2 つのブラウザータブで開いていると、少し不都合が生じます。

どちらのページもローカルストレージから読み込んだデータを画面に表示しているとしましょう。もし一方のページでストレージデータが更新されたとします。このページでは、データを更新したのは自分自身ですので、自身のページの表記を更新することができます。しかし、もう一方のタブで開いているページは古い情報を表示したままになってしまいます。

これを解決するのが、Web storage データが更新されたことを通知してくれるイベントです。ローカルストレージのデータ変更を伴う操作を行うと、window オブジェクトで storage イベントが発生します。具体的には Storage オブジェクトの setItem(), removeItem(), clear() メソッドが実行されると、window オブジェクトで storage イベントが発生します。

次の例はページにアクセスした回数をローカルストレージに保存して、それを画面に表示します。

<span id="counter">0</span>
// ローカルストレージの Storage オブジェクト
const storage = window.localStorage;

// カウンターを読み取る
let cnt = storage.getItem('counter');
cnt = cnt ? parseInt(cnt, 10) : 0; // String から Number に型変換

// カウンターをインクリメントしてストレージに保存する
storage.setItem('counter', ++cnt);

// カウンターを画面に表示する
document.getElementById('counter').textContent = cnt;

// storage イベントのリスナーをセット
window.addEventListener('storage', (evnet) => {
  if (event.key === 'counter') {
    document.getElementById('counter').textContent = event.newValue;
  }
});

上記と同じコードが 2 つのページに記述されていたとしましょう。また、それぞれの HTML ファイルは storage1.htmlstorage2.html とします。これらを別々のブラウザーウィンドウで開いているとします。すると、一方を再読み込みすると、もう一方では window オブジェクトで storage イベントをキャッチし、カウンターも自動的に更新表示されます。

上記のコードでは、storage イベントのリスナー関数でイベントオブジェクト(変数 event)を受け取っていますが、これは StorageEvent オブジェクトと呼びます。このオブジェクトには、次のプロパティがセットされています。

プロパティ説明
keyString変更が発生したデータのキー名
oldValueString変更が発生する前の値
newValueString変更が発生した後の値
urlString変更の発生元となるページの URL
storageAreaStorage変更が発生したストレージの Storage オブジェクト
StorageEvent オブジェクトのプロパティ

前述のサンプルでは、キー名 counter のデータを更新しましたので、StorageEvent オブジェクトの key プロパティの値は counter になります。

もし storage2.html を再読み込みしたなら、storage1.html 側で得られる StorageEvent オブジェクトの url プロパティの値は https://example.jp/storage2.html のような URL となります。

容量オーバーの挙動

Web storage のストレージ容量は 5 MB ~ 10 MB なのですが、これを超えてデータを保存しようとしたらどうなるでしょうか。実は Storage オブジェクトの setItem() メソッドを呼び出した際に DOMException 例外が投げられます。

残念ながら、今のところ JavaScript から Web storage のストレージがどれほど使用済みなのかを事前に調べる方法はありません。あまりストレージ容量の限界に近いデータを保存するようなことはしないほうが良いでしょう。

非同期ではない API

Indexed Database API や File System Access API といったストレージ系 API は非同期な API として設計されています。しかし、これまでのサンプルコードを見てお気づきの通り、Web storage は非同期な API ではありません。

ご存じの通り、ブラウザーのタブはそれぞれシングルスレッドで動作します。もし JavaScript の処理が重い、または、待ちが発生してしまうと、そのタブは固まった状態になってしまいます。そのため、とりわけディスクアクセスが発生するような API では、非同期な API として設計するのが当たり前です。

もちろん、ブラウザーはできる限りブロッキングが発生しないよううまく作られているのでしょうが、ディスクアクセスが発生する限り限界はあります。

そう考えると、Web storage にはあまり大きなデータを一気に書き込むことは避けるべきでしょう。Web storage のストレージ容量は 5 MB ~ 10 MB と他のストレージ系 API の容量と比べればかなり小さいですが、これだけのサイズのデータを頻繁に書き込めば、ブロッキング避けられないはずです。

データの保存期間

Web storage は永続的なストレージ API として作られたわけですが、もちろん、ユーザーが意図的に削除することは可能です。そのため、データが永久的に残る前提でウェブアプリを設計してはいけないことは言うまでもありません。とはいえ、一般のユーザーが意図的に削除することはめったにないため、事実上、永久的に残ると言っても良かったのも事実です。

ところが Apple 社が、広告業界などによるユーザー行動の追跡を排除するべく、プライバシー保護を名目に ITP (Tracking Prevention in WebKit) を推し進めた結果、Safari においては Cookie のみならず Web storage も制限の対象になってしまいました。

ITP により、Web storage によって保存されたデータは、最後にユーザーがサイトにアクセスしてから 7 日で削除されてしまいます。もちろん、ユーザーが 7 日経たずして定期的にサイトに訪れてくれれば良いのですが、どんなに人気のサイトと言えども、Web storage のデータは消されてしまう可能性は非常に高いと言えるでしょう。

ITP は Safari だけの問題ですので Safari のシェアが気になるところです。2022 年現在、Safari のシェアはデスクトップでは 6% 程度ですが、モバイルでは 60% にも及びます。そのため、Web storage のデータは事実上 7 日しか持たない前提でウェブアプリを設計する必要があります。

一方、Google は Privacy Sandbox と呼ばれるプライバシー保護対策を講じると発表しています。サードパーティ Cookie を段階的に廃止するはずだったのですが、何度か延期されています。直近では 2022 年 7 月にも延期が発表されました。2023 年 Q3 から開始する予定だそうです。今のところ、Web storage のデータは制限の対象になっていないようです。

まとめ

Web storage は永続的にデータを保存する仕組みとしては最もシンプルで使いやすいとはいえ、意外にも様々なユースケースを想定した機能が用意されています。わざわざ難しい API を使う前に、Web storage で事足りるかどうかしっかりと見極めたいですね。

今回は以上で終わりです。最後まで読んでくださりありがとうございました。それでは次回の記事までごきげんよう。

Share