JavaScript | 非同期処理:パフォーマンス最適化 - ネットワーク最適化

JavaScript JavaScript
スポンサーリンク

ネットワーク最適化って何をすること?

非同期処理のパフォーマンス最適化の中で、
ネットワーク最適化は「サーバーとの通信をできるだけ速く・少なく・無駄なくする」ことです。

JavaScript の世界だと、だいたいこういうところをいじります。

  • リクエストの回数を減らす
  • リクエストを投げるタイミングを賢くする
  • 1回あたりのデータ量を減らす
  • すでに持っているデータは再利用する(キャッシュ)

一個一個は地味ですが、積み重ねると体感がガラッと変わります。


リクエスト回数を減らす(まずここが一番効く)

同じ API を何度も叩いていないか?

一番分かりやすい無駄は、「同じデータを何度も取りに行っている」 パターンです。

例えば、画面のあちこちでユーザー情報を使うコード。

async function fetchUser() {
  const res = await fetch("/api/user");
  return res.json();
}

async function renderHeader() {
  const user = await fetchUser();
  // ヘッダーに表示
}

async function renderSidebar() {
  const user = await fetchUser();
  // サイドバーに表示
}
JavaScript

これだと、fetchUser() が呼ばれるたびに /api/user が叩かれます。

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

let userPromise = null;

function getUser() {
  if (!userPromise) {
    userPromise = fetch("/api/user").then((res) => res.json());
  }
  return userPromise;
}

async function renderHeader() {
  const user = await getUser();
}

async function renderSidebar() {
  const user = await getUser();
}
JavaScript

これで、
「最初の1回だけネットワークに行き、あとは同じ Promise を共有」
という状態になります。

ネットワーク最適化の第一歩は、
「同じものを何度も取りに行っていないか?」を疑うことです。

バッチ処理で「まとめて1回」にできないか?

もう一つよく効くのが バッチ処理 です。

例えば、ID ごとにユーザーを取る API があるとします。

async function fetchUser(id) {
  const res = await fetch(`/api/user?id=${id}`);
  return res.json();
}
JavaScript

これを 100 人分ループで呼ぶと、
100 回リクエストが飛びます。

もしサーバー側に「複数 ID をまとめて受け取れる API」があるなら、
こう書き換えられます。

async function fetchUsers(ids) {
  const res = await fetch("/api/users", {
    method: "POST",
    body: JSON.stringify({ ids }),
    headers: { "Content-Type": "application/json" },
  });
  return res.json();
}
JavaScript

100 回 → 1 回。
これだけで、ネットワークのオーバーヘッドが一気に減ります。

「同じ種類のリクエストを何度も飛ばしているところは、まとめられないか?」
この視点はかなり強いです。


並列化と直列化をちゃんと選ぶ(待ち時間を短くする)

独立したリクエストは同時に飛ばす

ネットワークは「待ち時間」が大きいので、
「待ちを重ねない」 ことが重要です。

悪い例:

async function loadPage() {
  const user = await fetch("/api/user");       // 1秒
  const posts = await fetch("/api/posts");     // 1秒
  const notifications = await fetch("/api/notifications"); // 1秒
}
JavaScript

これだと、合計 3 秒待つことになります。

良い例:

async function loadPage() {
  const userPromise = fetch("/api/user");
  const postsPromise = fetch("/api/posts");
  const notificationsPromise = fetch("/api/notifications");

  const [userRes, postsRes, notificationsRes] = await Promise.all([
    userPromise,
    postsPromise,
    notificationsPromise,
  ]);

  const [user, posts, notifications] = await Promise.all([
    userRes.json(),
    postsRes.json(),
    notificationsRes.json(),
  ]);
}
JavaScript

これなら、
「一番遅いものに合わせて待つだけ」 なので、体感はかなり軽くなります。

ポイントは、
「結果同士が依存していないなら、順番に await しない」
ということです。


データ量を減らす(いらないものはそもそも送らない)

本当にその全部のデータが必要か?

ネットワーク最適化でよく忘れられるのが、
「そもそもそんなにデータいらないのでは?」 という視点です。

例えば、サーバーからこんな JSON が返ってくるとします。

{
  "id": 1,
  "name": "太郎",
  "email": "taro@example.com",
  "address": { ...大きい... },
  "settings": { ...大きい... },
  "logs": [ ...大量... ]
}

でも、画面で使うのは idname だけ、
ということはよくあります。

そういうときは、
サーバー側に「軽いレスポンス」を用意してもらうのが理想です。

GET /api/user/summary
→ { "id": 1, "name": "太郎" }
JavaScript

もしサーバーを触れないなら、
クライアント側で「必要なものだけ取り出して捨てる」ことになりますが、
本当に効くのは“最初から送らない”こと です。

ネットワークは「距離」と「量」に弱いので、
「遠くから重い荷物を運ばない」 のが正義です。


キャッシュをちゃんと使う(同じものは二度取りに行かない)

HTTP キャッシュ(ブラウザの仕組み)を味方にする

ブラウザには、
「同じ URL のレスポンスを覚えておいて、次回は再利用する」
という HTTP キャッシュの仕組みがあります。

これは主にサーバー側の設定(レスポンスヘッダー)で制御しますが、
フロントエンドを書くときも意識しておくと得です。

例えば、
/api/master/categories のような「めったに変わらないデータ」は、
サーバー側で長めのキャッシュを設定しておくと、
2回目以降はネットワークに行かず、ローカルから返ってきます。

あなたがフロントエンド側でできることは、

  • 「これはキャッシュしても安全なデータか?」を見極めて
  • バックエンドの人に「ここキャッシュしたい」と相談する

ことです。

アプリ内キャッシュ(Promise キャッシュなど)

HTTP キャッシュとは別に、
アプリ内で「自前のキャッシュ」を持つのもよくあります。

さっきの getUser() のように、
「同じセッション中は同じデータを使い回す」 という発想です。

const cache = new Map();

async function fetchWithCache(url) {
  if (cache.has(url)) {
    return cache.get(url);
  }
  const promise = fetch(url).then((res) => res.json());
  cache.set(url, promise);
  return promise;
}
JavaScript

これで、
同じ URL に対するリクエストは
最初の1回だけになります。

ここが重要です。
「ネットワーク最適化=“取りに行かないで済むようにする”工夫の集合体」
と言ってもいいくらい、キャッシュは本質的な武器です。


無駄なリクエストをそもそも発生させない(UI 側の工夫)

入力のたびに API を叩いていないか?

検索ボックスで、
ユーザーが1文字入力するたびに API を叩くような実装は、
そのままだとかなり無駄が多いです。

input.addEventListener("input", async (e) => {
  const keyword = e.target.value;
  const res = await fetch(`/api/search?q=${encodeURIComponent(keyword)}`);
  const result = await res.json();
  render(result);
});
JavaScript

これだと、
「ta」と打つだけで tta の2回リクエストが飛びます。

ここで効いてくるのが デバウンス です。

「入力が止まってから 300ms 経ったら API を叩く」
というようにすると、
無駄なリクエストがかなり減ります。

function debounce(fn, delay) {
  let timer = null;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

const handleInput = debounce(async (keyword) => {
  const res = await fetch(`/api/search?q=${encodeURIComponent(keyword)}`);
  const result = await res.json();
  render(result);
}, 300);

input.addEventListener("input", (e) => {
  handleInput(e.target.value);
});
JavaScript

これで、
「ユーザーがある程度打ち終わってから、1回だけリクエスト」
という動きになります。

ネットワーク最適化は、
「ユーザーの操作に対して、どのタイミングで本当にリクエストが必要か?」
を丁寧に設計することでもあります。


初心者として「ネットワーク最適化」で本当に押さえてほしいこと

難しい言葉を全部捨ててしまうと、
ネットワーク最適化は次の3つに集約されます。

同じものを何度も取りに行かない
Promise キャッシュ・HTTP キャッシュ・アプリ内キャッシュを意識する。

まとめられるものはまとめる
バッチ処理・並列化で「回数」と「待ち時間の重なり」を減らす。

そもそも無駄なリクエストを発生させない
UI の入力頻度をデバウンスしたり、「本当に今必要か?」を考える。

どれか一つだけでもいいので、
自分のコードの中から「ネットワークに関わる処理」を一つ選んで、
こう問いかけてみてください。

「これ、
・回数を減らせないか?
・待ち時間を重ねられないか?
・そもそも要らないリクエストは混ざっていないか?」

その問いを持てるようになったとき、
あなたはもう「動くコードを書く人」から、
「速くて優しいコードを設計する人」 に足を踏み入れています。

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