JavaScript | 非同期処理:パフォーマンス最適化 - Promise キャッシュ

JavaScript JavaScript
スポンサーリンク

Promise キャッシュって何者?

Promise キャッシュは、
「同じ非同期処理を何度も実行せず、“進行中 or 結果”を使い回すテクニック」 です。

特に、
「同じ API を短時間に何回も叩いてしまう」
「同じデータを何度も取りに行ってしまう」
といったムダを減らすために使います。

ポイントは、
「結果の値」ではなく「Promise 自体」をキャッシュする
というところです。

これが分かると、一気に非同期のパフォーマンス設計が楽になります。


まずは「Promise をキャッシュしない」状態を見てみる

同じデータを毎回取りに行ってしまう例

例えば、ユーザー情報を取得する関数があるとします。

async function fetchUser() {
  console.log("API 呼び出し");
  await new Promise((r) => setTimeout(r, 1000)); // 1秒かかるとする
  return { id: 1, name: "太郎" };
}

async function main() {
  const user1 = await fetchUser();
  const user2 = await fetchUser();
  const user3 = await fetchUser();
}

main();
JavaScript

この場合、
fetchUser() が3回呼ばれるたびに、
API が3回叩かれ、毎回1秒ずつ待つことになります。

ログはこうなります。

API 呼び出し
API 呼び出し
API 呼び出し

「同じユーザー情報でいいのに、毎回取りに行っている」
これはパフォーマンス的にかなりもったいない状態です。


Promise をキャッシュする、という発想

「一度始めた非同期処理を、みんなで共有する」

ここで出てくるのが Promise キャッシュです。

やりたいことはシンプルで、

一度 fetchUser() を呼んだら、その Promise をどこかに保存しておく
次に同じものが欲しくなったら、新しく呼ばずに「保存しておいた Promise」を返す

という動きに変えることです。

コードで書くと、こうなります。

let userPromise = null;

function getUser() {
  if (!userPromise) {
    userPromise = fetchUser(); // 初回だけ実際に呼ぶ
  }
  return userPromise; // 2回目以降は同じ Promise を返す
}
JavaScript

そして、使う側はこう書きます。

async function main() {
  const user1 = await getUser();
  const user2 = await getUser();
  const user3 = await getUser();
}

main();
JavaScript

このときのログはこうなります。

API 呼び出し

1回しか呼ばれていません。

ここが重要です。
「Promise をキャッシュする」とは、“進行中 or 完了済みの非同期処理”を変数に覚えておき、
同じものを欲しがる人たちにそれを配る、ということ。
結果の値ではなく、Promise そのものを共有する。


なぜ「Promise 自体」をキャッシュするのか

進行中も、完了後も、同じインターフェースで扱えるから

Promise の良いところは、
「まだ終わっていなくても」「もう終わっていても」
await で同じように扱えることです。

進行中の Promise を await すれば、終わるまで待つ
すでに解決済みの Promise を await すれば、すぐ値が返る

Promise をキャッシュする、というのは
この性質をフル活用しています。

let userPromise = null;

function getUser() {
  if (!userPromise) {
    userPromise = fetchUser();
  }
  return userPromise;
}
JavaScript

最初の呼び出しでは、
fetchUser() が走り始めた Promise を返す。

2回目以降の呼び出しでは、
すでに進行中 or 完了済みの Promise をそのまま返す。

呼び出し側は、
「今が初回かどうか」「もう終わっているかどうか」を気にせず、
ただ await getUser() と書くだけでよくなります。

ここが重要です。
「Promise をキャッシュすると、“状態管理”を中に閉じ込められる。
呼び出し側は、ただ await するだけでよくなる。」


引数付きの Promise キャッシュ(キーごとにキャッシュする)

ユーザーIDごとにキャッシュしたい場合

現実のコードでは、
「ユーザーIDごとに API を叩く」ようなケースが多いです。

async function fetchUser(id) {
  console.log("API 呼び出し:", id);
  await new Promise((r) => setTimeout(r, 1000));
  return { id, name: `User ${id}` };
}
JavaScript

これを「IDごとにキャッシュ」したいなら、
オブジェクトや Map を使って「Promise の辞書」を作ります。

const userPromiseCache = new Map();

function getUser(id) {
  if (!userPromiseCache.has(id)) {
    const promise = fetchUser(id);
    userPromiseCache.set(id, promise);
  }
  return userPromiseCache.get(id);
}
JavaScript

使う側はこうです。

async function main() {
  const u1 = await getUser(1);
  const u2 = await getUser(1);
  const u3 = await getUser(2);

  console.log(u1, u2, u3);
}

main();
JavaScript

ログはこうなります。

API 呼び出し: 1
API 呼び出し: 2

getUser(1) は何度呼んでも、
最初の1回しか API が叩かれません。

ここが重要です。
「Promise キャッシュ」は、“引数の組み合わせごとに一度だけ実行したい非同期処理”に特に効く。
Map やオブジェクトを使って「キー → Promise」を覚えておくのが定番パターン。


エラー時の扱い(ここは少しだけ注意が必要)

失敗した Promise をキャッシュし続けていいのか?

Promise キャッシュでよく出てくる論点が、
「失敗したときどうするか」です。

今の実装だと、
fetchUser(id) がエラーになった場合でも、
その reject された Promise がキャッシュに残ります。

const userPromiseCache = new Map();

function getUser(id) {
  if (!userPromiseCache.has(id)) {
    const promise = fetchUser(id);
    userPromiseCache.set(id, promise);
  }
  return userPromiseCache.get(id);
}
JavaScript

一度失敗すると、
次に getUser(id) を呼んだときも、
同じ失敗した Promise が返ってきて、
毎回同じエラーになります。

それでいい場合もありますが、
「失敗したら、次の呼び出しではもう一度トライしたい」
というケースも多いです。

その場合は、
エラー時にキャッシュを消す、という工夫をします。

function getUser(id) {
  if (!userPromiseCache.has(id)) {
    const promise = fetchUser(id).catch((err) => {
      userPromiseCache.delete(id); // 失敗したらキャッシュから消す
      throw err;                   // エラーは呼び出し側に投げ直す
    });
    userPromiseCache.set(id, promise);
  }
  return userPromiseCache.get(id);
}
JavaScript

こうすると、

成功した場合:Promise がキャッシュに残り、次回以降も使われる
失敗した場合:キャッシュから消えるので、次回は新しく fetchUser が呼ばれる

という挙動になります。

ここが重要です。
「Promise キャッシュは“成功結果を再利用する”のが基本。
失敗をどう扱うかは、要件に応じて設計する必要がある。」


Promise キャッシュが効く典型的な場面

1. 同じ画面内で、同じデータを何度も使う

例えば、
ユーザー情報をヘッダー・サイドバー・メインコンテンツでそれぞれ表示するような画面。

何も考えずに書くと、
それぞれが fetchUser() を呼んでしまい、
同じ API が3回叩かれます。

Promise キャッシュを使って getUser() を共通化すれば、
最初の1回だけ API を叩き、
あとは同じ Promise を共有できます。

2. タブ切り替えなどで、同じデータに何度も戻ってくる

「一度読み込んだリストに、タブを切り替えて戻ってくる」
といった UI でも、
Promise キャッシュを使うと「戻ってきたときに即座に表示」ができます。

ここが重要です。
「“一度取ったデータ”を、同じセッション中で何度も使う」
という場面では、Promise キャッシュはかなり強力な武器になる。


初心者として「Promise キャッシュ」で本当に押さえてほしいこと

最後に、要点をコンパクトにまとめます。

Promise キャッシュとは、
「同じ非同期処理の Promise を変数や Map に保存して、再利用すること」 です。

一度始めた非同期処理を、
複数の呼び出しで共有できるので、

同じ API を何度も叩かなくて済む
同じデータを何度も待たなくて済む

というメリットがあります。

特に大事なのは、この感覚です。

結果の「値」ではなく「Promise 自体」をキャッシュする
→ 進行中でも完了済みでも、await で同じように扱えるから

引数ごとにキャッシュしたいときは「キー → Promise」の Map を使う
失敗時にどうするか(キャッシュを残すか消すか)は要件次第

まずは、次のようなコードを自分で書いてみてください。

async function fetchUser() {
  console.log("API 呼び出し");
  await new Promise((r) => setTimeout(r, 1000));
  return { id: 1, name: "太郎" };
}

let userPromise = null;

function getUser() {
  if (!userPromise) {
    userPromise = fetchUser();
  }
  return userPromise;
}

async function main() {
  const u1 = await getUser();
  const u2 = await getUser();
  const u3 = await getUser();
  console.log(u1, u2, u3);
}

main();
JavaScript

コンソールに「API 呼び出し」が1回しか出ないのを見て、
「あ、Promise をキャッシュするってこういうことか」と
身体で理解してみてください。

そこから先は、
「どの非同期処理をキャッシュすると気持ちいいか?」を
自分のアプリに照らして考えていくフェーズです。
その思考こそが、パフォーマンス最適化の本番です。

タイトルとURLをコピーしました