ブラウザのキャッシュAPIを試してみた

# 背景・目的
ブラウザのキャッシュAPIについて調べる機会があったので、簡単に整理し試してみます。

# まとめ

下記に特徴をまとめます

|特徴|説明|
|:--|:--|
|Cacheインターフェイス|・Request/Responseオブジェクトのペアを保存するストレージの仕組みを提供する<BR>・Cache オブジェクトがどのくらい持続するかはブラウザーに依存する<BR>・サービスワーカーだけでなく、ブラウザのウィンドウスコープ（通常のウェブページ）」でも使える|
|キャッシュの管理|・ひとつのオリジンが、複数の名前付き Cache オブジェクトを保有することができる<BR>・複数の名前付きキャッシュを持てる<BR>・キャッシュ更新の管理は開発者の責任<BR>・CacheStorage.open() で特定の名前のキャッシュを開く|

# 概要
下記を基に整理します

https://developer.mozilla.org/ja/docs/Web/API/Cache

## Cache
> Cache インターフェイスは、 Request / Response オブジェクトのペアを保存するストレージの仕組みを提供します。Cache オブジェクトがどのくらい持続するかはブラウザーに依存しますが、単一のオリジンのスクリプトは通常、以前に生成された Cache オブジェクトの存在を頼りにすることができます。Cache インターフェイスは、ワーカーと同様にウィンドウスコープにも公開されていることに注意してください。サービスワーカーの仕様で定義されているとはいえ、サービスワーカーと共に使用する必要はありません。

- Cacheインターフェイスは、Request/Responseオブジェクトのペアを保存するストレージの仕組みを提供
- Cache オブジェクトがどのくらい持続するかはブラウザーに依存する
- サービスワーカーだけでなく、ブラウザのウィンドウスコープ（通常のウェブページ）」でも使える

> ひとつのオリジンが、複数の名前付き Cache オブジェクトを保有することができます。スクリプト（例えば ServiceWorker） が Cache の更新をどのように処理するかを実装するのは作者の責任です。Cache 内のアイテムは、明示的に要求されない限り更新されませんし、削除されない限り有効期限はありません。 CacheStorage.open() を使用して特定の名前付き Cache オブジェクトを開き、それから任意の Cache のメソッドを呼び出して Cache を管理します。

- ひとつのオリジンが、複数の名前付き Cache オブジェクトを保有することができる
- 複数の名前付きキャッシュを持てる
- キャッシュ更新の管理は開発者の責任
- CacheStorage.open() で特定の名前のキャッシュを開く

> また、定期的にキャッシュ項目を掃除する必要があります。各ブラウザーは、あるオリジンで使用できるキャッシュストレージの総量に厳しい制限を設けています。 Cache 容量の概算の使用量は StorageManager.estimate() API を用いて確認することができます。ブラウザーはディスク容量の管理に最善を尽くしますが、あるオリジンのキャッシュストレージを削除することがあります。ブラウザーはふつう、あるオリジンのデータをすべて削除するか、まったく削除しないかのいずれかです。名前を用いてキャッシュをバージョン管理し、安全に操作できるスクリプトのバージョンからのみキャッシュを使用するようにしてください。詳細は、古いキャッシュの削除を確認してください。

- 各ブラウザーは、あるオリジンで使用できるキャッシュストレージの総量に厳しい制限を設けている
    - Cache 容量の概算の使用量は StorageManager.estimate() API を用いて確認できる
- ブラウザによる自動キャッシュ削除の可能性がある


# 実践
下記のようなシナリオで簡単に動作確認をします。

1. キャッシュ容量の使用量確認
1. 名前付きキャッシュのバージョン管理とデータ保存
1. データの取得とキャッシュの活用
1. 定期的なキャッシュクリーニング

## 前提
- VSCode
- Mac OS


## 事前準備
1. プロジェクトフォルダを作成します
    ```bash
    git % mkdir frontend-cache-test
    ```
1. VSCodeでプロジェクトをワークスペースに追加します

## 1. キャッシュ容量の使用量確認
1. `cache-scripts/storageCheck.js`を作成し、下記を実装します
    ```js
    // storageCheck.js
    export async function checkCacheStorage() {
      if ('storage' in navigator && 'estimate' in navigator.storage) {
          // StorageManager.estimate()を使用して、キャッシュ容量の使用料と総容量を確認します
          const estimate = await navigator.storage.estimate();
          console.log(`使用容量: ${(estimate.usage / 1024 / 1024).toFixed(2)} MB`);
          console.log(`総容量: ${(estimate.quota / 1024 / 1024).toFixed(2)} MB`);
      }
    }
    ```
1. `assets/styles.css`を作成し、下記を実装します
    ```css
    /* assets/styles.css */
    
    body {
        font-family: Arial, sans-serif;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        height: 100vh;
        margin: 0;
        background-color: #f4f4f9;
    }
    
    h1 {
        color: #333;
        font-size: 24px;
        margin-bottom: 20px;
    }
    
    button {
        padding: 10px 20px;
        font-size: 16px;
        color: #fff;
        background-color: #007bff;
        border: none;
        border-radius: 5px;
        cursor: pointer;
    }
    
    button:hover {
        background-color: #0056b3;
    }
    
    button:focus {
        outline: none;
    }
    ```
1. `app.js`を作成し、下記を実装します
    ```js
    import { checkCacheStorage } from './cache-scripts/storageCheck.js';
    
    const currentCache = 'my-cache-v1';
    
    // キャッシュの管理機能を実行するメイン関数
    async function runApp() {
        // 1. キャッシュの容量確認
        await checkCacheStorage();
    
    }
    ```

1. `index.html`を作成し、下記を実装します
    ```html
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Cache API Demo</title>
        <link rel="stylesheet" href="assets/styles.css">
    </head>
    <body>
        <h1>Cache API Demo</h1>
        <button onclick="runApp()">Run Cache Operations</button>
        <script type="module" src="app.js"></script>
    </body>
    </html>
    ```

1. ローカルサーバを使用して確認します
    ```bash
    frontend-cache-test % npx http-server .
    Starting up http-server, serving .
    ```

1. ブラウザに`localhost:8080`を入力し確認します。表示されました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/b4fd67b4-b8b7-bfa4-6b29-d42f217a1fb3.png)

1. 「Run Cache Operations」をクリックします。Consoleに下記が表示されました
    - 使用容量: 0.00 MB
    - 総容量: 1083.16 MB
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/e996be96-e843-5460-9a94-c68953154131.png)


## 2. 名前付きキャッシュのバージョン管理とデータ保存
1. `cache-scripts/cacheManager.js`を作成し、下記を実装します
    ```js
    // cacheManager.js
    export async function saveToCache(data, cacheName) {
        const cache = await caches.open(cacheName);
        await cache.put('/demo-data', new Response(JSON.stringify(data)));
        console.log(`${cacheName} にデータが保存されました`);
    }
    ```
1. `app.js`を修正します（2を追加します）

    ```js
    // app.js
    import { checkCacheStorage } from './cache-scripts/storageCheck.js';
    
    // 2で追加
    import { saveToCache } from './cache-scripts/cacheManager.js';
    
    
    const currentCache = 'my-cache-v1';
    
    // キャッシュの管理機能を実行するメイン関数
    async function runApp() {
        // 1. キャッシュの容量確認
        await checkCacheStorage();
    

        // 2. 名前付きキャッシュへのデータ保存
        // ダミーデータとして大きな配列を作成
        const dummyData = {
          content: 'オフライン用のダミーデータ',
          largeData: Array(10000).fill('データサンプル')
        };
        await saveToCache(dummyData, currentCache);

    }
    
    // グローバルスコープにrunAppを登録
    window.runApp = runApp;
    
    ```
1. ローカルサーバを再起動します
1. ブラウザに`localhost:8080`を入力し確認します
1. 「Run Cache Operations」をクリックします。Consoleに下記が表示されました
    - 「my-cache-v1 にデータが保存されました」
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/f6903f8a-328a-2142-552b-9c06e079cd5b.png)
1. もう一度クリックすると使用容量が増えました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/e71204d6-cac5-c5bf-0c45-4fc2600c24c6.png)


## 3.データの取得とキャッシュの活用
1. `cache-scripts/fetchData.js`を作成し、下記を実装します
    - オフライン時には、キャッシュAPIを利用してキャッシュからデータを取得します
    - オンライン時には、APIからデータを取得し、saveToCache関数を使用してキャッシュにも保存します

    ```js
    // fetchData.js
    import { saveToCache } from './cacheManager.js';
    
    // キャッシュまたはネットワークからデータを取得する関数
    export async function fetchData(cacheName) {
        const cache = await caches.open(cacheName);
    
        if (!navigator.onLine) {
            // オフライン時にはキャッシュからデータを取得
            const cachedResponse = await cache.match('/demo-data');
            if (cachedResponse) {
                return await cachedResponse.json(); // キャッシュからデータを取得
            } else {
                return { content: 'オフラインですがキャッシュデータがありません' }; // キャッシュが空の場合
            }
        } else {
            // オンライン時にはダミーデータを保存してから取得
            const data = { content: 'オンラインからのデータ', largeData: Array(10000).fill('データサンプル') };
            await saveToCache(data, cacheName); // ダミーデータをキャッシュに保存
            return data; // 保存したデータを返す
        }
    }

    ```

1. app.jsにfetchDataをインポートします
    ```js
    // app.js
    import { checkCacheStorage } from './cache-scripts/storageCheck.js';
    
    // 2で追加
    import { saveToCache } from './cache-scripts/cacheManager.js';
    
    // 3で追加
    import { fetchData } from './cache-scripts/fetchData.js';
    
    
    
    const currentCache = 'my-cache-v1';
    
    // キャッシュの管理機能を実行するメイン関数
    async function runApp() {
        // 1. キャッシュの容量確認
        await checkCacheStorage();
    
        // 2. 名前付きキャッシュへのデータ保存
        const data = { content: 'バージョン1のデータ' };
        await saveToCache(data, currentCache);
    
        // 3. データの取得とキャッシュの活用
        const fetchedData = await fetchData(currentCache);
        document.getElementById('output').innerText = fetchedData.content;
    
    
    }
    
    // グローバルスコープにrunAppを登録
    window.runApp = runApp;
    ```

1. `index.html`に表示用の要素を追加します
    ```html
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Cache API Demo</title>
        <link rel="stylesheet" href="assets/styles.css">
    </head>
    <body>
        <h1>Cache API Demo</h1>
        <button onclick="runApp()">Run Cache Operations</button>
    
        <!-- 取得したデータを表示する要素 -->
        <div id="output"></div>
    
    
        <script type="module" src="app.js"></script>
    </body>
    </html>
    ```
1. ローカルサーバを再起動します
1. ブラウザに`localhost:8080`を入力し確認します
1. 「Run Cache Operations」をクリックします。Consoleに下記が表示されました
    - オンラインからのデータ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/c9acf205-ad0a-2f24-41b4-6168bea16708.png)

1. NW接続を閉じます
1. 「Run Cache Operations」をクリックします。Consoleに下記が表示されました
    - オフライン用のダミーデータ

    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/e18a41c1-c918-505f-499a-330d498cda8e.png)



# 考察
今回、キャッシュAPIを用いて、データの保存、容量確認を行う操作を実装しました。ブラウザの開発者ツールを通じて、キャッシュが実際に保存されることを確認しました。
また、オンライン・オフラインでのキャッシュ利用の切り替えも試してみました。

今回の実装では、シンプルなダミーデータのキャッシュの確認のみを行いましたが、今後は以下についても試してみ用途思います。

- キャッシュの有効期限管理：キャッシュに有効期限を設定し、古いデータの自動更新や削除ができるようにする
- リアルタイムデータの管理：実際のAPIからの動的なデータを使い、キャッシュの更新を行う
- 大容量ファイルの管理：画像や動画などの大容量ファイルを効率的にキャッシュし、必要に応じて部分的に削除や更新を行う仕組みを試す


# 参考
https://developer.mozilla.org/ja/docs/Web/API/Cache

https://developer.mozilla.org/ja/docs/Web/API/Web_Storage_API

https://developer.mozilla.org/ja/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria#%E3%82%A6%E3%82%A7%E3%83%96%E3%82%B9%E3%83%88%E3%83%AC%E3%83%BC%E3%82%B8