ネットワーク最適化って何をすること?
非同期処理のパフォーマンス最適化の中で、
ネットワーク最適化は「サーバーとの通信をできるだけ速く・少なく・無駄なくする」ことです。
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();
}
JavaScript100 回 → 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": [ ...大量... ]
}
でも、画面で使うのは id と name だけ、
ということはよくあります。
そういうときは、
サーバー側に「軽いレスポンス」を用意してもらうのが理想です。
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」と打つだけで t と ta の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 の入力頻度をデバウンスしたり、「本当に今必要か?」を考える。
どれか一つだけでもいいので、
自分のコードの中から「ネットワークに関わる処理」を一つ選んで、
こう問いかけてみてください。
「これ、
・回数を減らせないか?
・待ち時間を重ねられないか?
・そもそも要らないリクエストは混ざっていないか?」
その問いを持てるようになったとき、
あなたはもう「動くコードを書く人」から、
「速くて優しいコードを設計する人」 に足を踏み入れています。
