「非同期パフォーマンス」は“待ち時間をどう見せるか”の技術
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 };
}
JavaScriptdata1 を取り終わってから、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 の見せ方、計測の仕方を少しずつ磨いていくだけです。
