JavaScript | Web API:パフォーマンス・セキュリティ - 非同期パフォーマンス

JavaScript JavaScript
スポンサーリンク

「非同期パフォーマンス」は“待ち時間をどう見せるか”の技術

JavaScript のパフォーマンスって聞くと、
「処理をどれだけ速くするか」をイメージしがちですが、
非同期の世界ではもう一つ大事な視点があります。

それは、
「待ち時間そのものは減らせなくても、体感を軽くできるか?」
という視点です。

サーバーからのレスポンスを待つ時間
setTimeout やアニメーションの待ち時間
複数の非同期処理をどう並べるか

こういう「待ち」が絡む部分の設計が、
非同期パフォーマンスの本質です。


メインスレッドを“塞がない”ことが最優先

重い処理を同期でやると、UI が固まる

ブラウザの JavaScript は、基本的にメインスレッドで動きます。
ここが重い処理で塞がると、次のようなことが起きます。

スクロールがカクカクする
クリックしても反応が遅い
入力しても文字がすぐ出てこない

例えば、こんなコードは典型的な「UI を固める」やつです。

function blockFor(ms) {
  const start = Date.now();
  while (Date.now() - start < ms) {
    // ひたすら待つ(CPU を占有)
  }
}

console.log("start");
blockFor(3000); // 3 秒間ブロック
console.log("end");
JavaScript

この間、画面はほぼ何もできません。
ユーザーからすると「フリーズした?」と感じるレベルです。

非同期パフォーマンスの第一歩は、
「こういう“メインスレッドを塞ぐ処理”を極力書かない」
という意識を持つことです。


非同期処理の「速さ」は“並べ方”で大きく変わる

直列で待つか、並列で待つか

例えば、API を 2 回叩くケースを考えます。

まずは「直列で待つ」パターン。

async function fetchSequential() {
  console.time("sequential");

  const res1 = await fetch("/api/data1");
  const data1 = await res1.json();

  const res2 = await fetch("/api/data2");
  const data2 = await res2.json();

  console.timeEnd("sequential");
  return { data1, data2 };
}
JavaScript

data1 を取り終わってから、data2 を取りに行きます。
片方が 500ms、もう片方が 600ms かかるとしたら、
合計で 1100ms くらい待つことになります。

次に「並列で待つ」パターン。

async function fetchParallel() {
  console.time("parallel");

  const p1 = fetch("/api/data1");
  const p2 = fetch("/api/data2");

  const [res1, res2] = await Promise.all([p1, p2]);
  const data1 = await res1.json();
  const data2 = await res2.json();

  console.timeEnd("parallel");
  return { data1, data2 };
}
JavaScript

ここでは、fetch を先に両方投げておいて、
Promise.all で「両方終わるのを待つ」形にしています。

この場合、片方が 500ms、もう片方が 600ms でも、
待ち時間は「長い方の 600ms 前後」で済みます。

非同期パフォーマンスでめちゃくちゃ重要なのは、
「本当に直列でやる必要があるか?」を常に疑うことです。
独立した処理なら、できるだけ並列に投げた方が体感は軽くなります。


「待ち時間」をどう見せるかで体感が変わる

何も起きない 1 秒と、「読み込み中」が見える 1 秒

非同期処理は、どうしても「待ち」が発生します。
その待ち時間を、ユーザーにどう感じさせるかが UX の勝負どころです。

例えば、ボタンを押したらデータを読み込むケース。

悪い例は、こうです。

button.addEventListener("click", async () => {
  const res = await fetch("/api/data");
  const data = await res.json();
  render(data);
});
JavaScript

押してから何も変化がないまま、
いきなり結果だけ出てきます。

良い例は、こうです。

button.addEventListener("click", async () => {
  button.disabled = true;
  button.textContent = "読み込み中…";

  try {
    const res = await fetch("/api/data");
    const data = await res.json();
    render(data);
  } finally {
    button.disabled = false;
    button.textContent = "読み込む";
  }
});
JavaScript

ここでは、

ボタンを押した瞬間に「読み込み中」と表示を変える
二重クリックを防ぐために disabled にする
終わったら元に戻す

という工夫をしています。

非同期パフォーマンスの本質は、
「待ち時間をゼロにする」ことではなく、
「待っていることが分かる状態にする」 ことです。


非同期でも「メインスレッドを詰まらせる」罠

Promise や async/await でも、重い処理は重いまま

よくある誤解として、
「async/await にしたから軽くなった」という勘違いがあります。

例えば、こういうコード。

async function heavyAsync() {
  const arr = [];
  for (let i = 0; i < 200000000; i++) {
    arr.push(i);
  }
  return arr.length;
}
JavaScript

これを呼ぶ側が async/await でも、

async function run() {
  console.log("start");
  const len = await heavyAsync();
  console.log("end", len);
}
JavaScript

中身の for ループは、
結局メインスレッドでガッツリ回ります。
UI はその間固まります。

非同期というのは、
「重い処理を別スレッドに逃がす魔法」ではありません。
あくまで「待ち時間の扱い方」を変える仕組みです。

本当に重い計算をメインスレッドから逃がしたいなら、
Web Worker など別スレッドの仕組みを使う必要があります。


非同期パフォーマンスを測るときのポイント

「いつ始まって、いつ終わったか」をちゃんと記録する

非同期処理のパフォーマンスを測るときも、
基本は同期と同じで「前後を測る」です。

ただし、async/await の前後で測るのがコツです。

async function fetchWithMeasure() {
  const start = performance.now();

  const res = await fetch("/api/data");
  const data = await res.json();

  const end = performance.now();
  console.log(`fetch + json にかかった時間: ${(end - start).toFixed(2)} ms`);

  return data;
}
JavaScript

ここで測っているのは、

ネットワークの待ち時間
レスポンスのパース時間

を合わせた「体感の待ち時間」です。

さらに細かく見たいなら、
performance.mark / measure を使ってもいいです。

async function fetchWithMarks() {
  performance.mark("fetch-start");
  const res = await fetch("/api/data");
  performance.mark("fetch-end");
  performance.measure("fetch", "fetch-start", "fetch-end");

  performance.mark("json-start");
  const data = await res.json();
  performance.mark("json-end");
  performance.measure("json", "json-start", "json-end");

  const fetchMeasure = performance.getEntriesByName("fetch")[0];
  const jsonMeasure = performance.getEntriesByName("json")[0];

  console.log(`fetch: ${fetchMeasure.duration.toFixed(2)} ms`);
  console.log(`json: ${jsonMeasure.duration.toFixed(2)} ms`);

  return data;
}
JavaScript

こうすると、

「ネットワークが遅いのか」
「JSON のパースが重いのか」

が数字で分かります。


非同期パフォーマンスで初心者が意識しておくと強いポイント

非同期パフォーマンスは、
いきなり難しいことを全部やる必要はありません。

まずは、次の感覚を持てれば十分です。

メインスレッドを長時間ブロックする処理は書かない
独立した非同期処理は、できるだけ並列に投げる(Promise.all を意識する)
待ち時間はゼロにできなくても、「待っていることが分かる UI」にする
async/await にしても、重い計算は軽くならない(必要なら Web Worker を検討)
非同期処理の前後で時間を測って、「どこで待っているか」を数字で見る

あなたがコードを書きながら、
「これ、ユーザーから見たらどこで待たされてるんだろう?」
と一度でも考えられるようになったら、
それはもう非同期パフォーマンスの入り口に立てています。

そこから先は、
Promise の並べ方、UI の見せ方、計測の仕方を少しずつ磨いていくだけです。

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