レイテンシってそもそも何?
まず言葉からいきます。
レイテンシ(latency)は「お願いしてから、最初の反応が返ってくるまでの時間」 です。
ユーザーから見ると、
ボタンを押してから画面が変わるまで
検索ワードを打ってから結果が出るまで
ページを開いてからコンテンツが見えるまで
この「待たされている時間」がレイテンシです。
非同期処理のパフォーマンス最適化で大事なのは、
「このレイテンシをどう短くするか」だけじゃなく、
「どう“感じさせないか”」まで含めて考えることです。
レイテンシはどこで発生しているのか?
ネットワークのレイテンシ
一番分かりやすいのは、サーバーとの通信です。
const res = await fetch("/api/data");
const data = await res.json();
JavaScriptここには、いろんな待ち時間が混ざっています。
リクエストがブラウザからサーバーに届くまでの時間
サーバーが処理する時間
レスポンスがサーバーからブラウザに戻ってくる時間
これらの合計が「ネットワークレイテンシ」です。
同じコードでも、
東京から東京のサーバーにアクセスするのと、
東京からアメリカのサーバーにアクセスするのでは、
体感が全然違いますよね。
ここがポイントで、
「自分のコードが遅い」のか「ネットワークが遠い」のかを分けて考える癖
がつくと、一気に視界がクリアになります。
サーバー処理のレイテンシ
サーバー側で重い計算をしている場合も、
その分だけ待ち時間が増えます。
例えば、
巨大なレポートを生成する API を叩いたとき、
レスポンスが返ってくるまで数秒かかることがあります。
このとき、フロントエンド側でできることは限られますが、
「その数秒をどう扱うか」 は設計できます。
後で「隠し方」の話をします。
レイテンシを“短くする”ためにできること
無駄な待ちを重ねない(並列化)
典型的な「レイテンシを無駄に増やしている」コードはこれです。
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,
]);
}
JavaScriptこうすると、
「一番遅いものに合わせて待つだけ」になるので、
体感は 1 秒くらいになります。
ここが重要です。
レイテンシを短くする基本は「待ち時間を足し算にしないこと」。
独立した非同期処理は、できるだけ同時に走らせる。
先に始められるものは、できるだけ早く始める
例えば、ページ遷移のとき。
ユーザーが「詳細ページへ」のリンクをクリックした瞬間に
API を叩き始めるのではなく、
「リンクにマウスを乗せた瞬間」に先に叩き始める、というテクニックがあります。
let prefetchPromise = null;
link.addEventListener("mouseenter", () => {
if (!prefetchPromise) {
prefetchPromise = fetch("/api/detail").then((res) => res.json());
}
});
link.addEventListener("click", async (e) => {
e.preventDefault();
const data = await (prefetchPromise ?? fetch("/api/detail").then((res) => res.json()));
showDetail(data);
});
JavaScriptユーザーがクリックするまでの数百ミリ秒を使って、
裏でデータ取得を始めておくわけです。
ここで大事なのは、
「ユーザーの行動を少し先読みして、レイテンシを前倒しする」
という発想です。
レイテンシを“感じさせない”ためにできること
ローディング表示は「ただのスピナー」では足りない
レイテンシをゼロにできない場面は必ずあります。
そのときに効いてくるのが「見せ方」です。
一番シンプルなのはローディング表示ですが、
ただのクルクルだけだと、
ユーザーは「本当に動いているのか?」と不安になります。
例えば、こういう工夫ができます。
何%まで進んだかを見せる(プログレスバー)
「データを読み込み中です…」など、何をしているかを言葉で伝える
すぐに出せる部分だけ先に表示し、重い部分は後から埋める
コードでいうと、
「先に出せるものだけ出す」というのはこういうイメージです。
async function loadPage() {
const userPromise = fetch("/api/user").then((r) => r.json());
const postsPromise = fetch("/api/posts").then((r) => r.json());
const user = await userPromise;
renderUser(user); // 先にユーザー情報だけ表示
const posts = await postsPromise;
renderPosts(posts); // 投稿は後から追加表示
}
JavaScriptここが重要です。
「全部そろうまで真っ白で待たせる」のではなく、
「出せるところから順に出していく」ことで、
同じレイテンシでも“待たされている感”を減らせる。
楽観的 UI(optimistic UI)
もう一歩攻めたテクニックが「楽観的 UI」です。
例えば、「いいね」ボタン。
普通にやるとこうです。
async function onLikeClick() {
await fetch("/api/like", { method: "POST" });
updateLikeCountOnUI();
}
JavaScriptこれだと、
サーバーからのレスポンスを待つ間、
ボタンの表示は変わりません。
楽観的 UI では、
「どうせ成功するだろう」と楽観して、
先に UI を変えてしまいます。
async function onLikeClick() {
updateLikeCountOnUI(); // 先に増やす
try {
await fetch("/api/like", { method: "POST" });
} catch (e) {
rollbackLikeCountOnUI(); // 失敗したら戻す
}
}
JavaScriptユーザーから見ると、
クリックした瞬間に「いいね数」が増えるので、
レイテンシをほとんど感じません。
ここが重要です。
「結果を待ってから UI を変える」のではなく、
「先に UI を変えて、失敗したら調整する」という発想を持てると、
レイテンシの体感が劇的に変わる。
レイテンシを前提にした設計(タイムアウト・リトライ・優先度)
タイムアウトを決める
レイテンシを考えるとき、
「どこまで待つか」 を決めるのも大事です。
例えば、
「このサジェスト API は 500ms 以上かかるなら諦めて、
今ある結果だけで表示する」
といったルールを決めることがあります。
function withTimeout(promise, ms) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error("timeout")), ms);
promise
.then((v) => {
clearTimeout(timer);
resolve(v);
})
.catch((e) => {
clearTimeout(timer);
reject(e);
});
});
}
async function fetchWithLatencyLimit() {
const res = await withTimeout(fetch("/api/suggest"), 500);
return res.json();
}
JavaScript「永遠に待ち続ける」のではなく、
「この操作に許せるレイテンシの上限」を決めておく
のは、UX と安定性の両方に効きます。
優先度をつける
全部のリクエストを同じように扱うのではなく、
「ユーザーが今見ている部分」に関係するものを優先する
という考え方もあります。
例えば、
スクロールで下の方にある画像は、
今すぐではなく「近づいてきたら読み込む」で十分です。
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src; // 見えそうになったら読み込む
observer.unobserve(img);
}
}
});
document.querySelectorAll("img[data-src]").forEach((img) => {
observer.observe(img);
});
JavaScriptこれもレイテンシの一種です。
「見えていないものの読み込みは、わざと遅らせる」ことで、
今見えている部分の体感を良くしています。
ここが重要です。
レイテンシを考えるときは、「全部を速く」ではなく、
「今ユーザーが気にしているところを速く」する発想を持つ。
初心者として「レイテンシ考慮」で本当に押さえてほしいこと
最後に、感覚として持っておいてほしいのはこの3つです。
レイテンシは「待ち時間」そのものだけでなく、「どう感じられるか」まで含めて設計するもの
独立した処理は並列に、先読みできるものは前倒しに、出せる部分から順に表示する
「どこまで待つか」「何を優先するか」「先に UI を動かせないか」を常に意識する
おすすめの練習は、
自分がよく使う Web サービスを開いて、
「どこで待たされているか」「どこで先に UI だけ出しているか」を観察してみることです。
その上で、自分のコードに戻ってきて、
一つの非同期処理を選び、こう自問してみてください。
「この待ち時間、
短くできるか?
前倒しできるか?
感じさせないようにできるか?」
その問いを持ち続けることが、
レイテンシを“敵”ではなく“前提条件”として扱えるエンジニアへの一歩になります。
