JavaScript | 非同期処理:パフォーマンス最適化 - レイテンシ考慮

JavaScript JavaScript
スポンサーリンク

レイテンシってそもそも何?

まず言葉からいきます。
レイテンシ(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 だけ出しているか」を観察してみることです。

その上で、自分のコードに戻ってきて、
一つの非同期処理を選び、こう自問してみてください。

「この待ち時間、
短くできるか?
前倒しできるか?
感じさせないようにできるか?」

その問いを持ち続けることが、
レイテンシを“敵”ではなく“前提条件”として扱えるエンジニアへの一歩になります。

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