なぜ「UI ブロック回避」がそんなに大事なのか
JavaScript は基本「1本のメインスレッド」で動いています。
この1本の線の上で、描画・クリック処理・スクロール・あなたの書いた処理が、全部順番待ちしています。
だから、
重い処理をドカッとメインスレッドで走らせると、その間 UI が全部止まる
これが「UI ブロック」です。
ユーザーから見ると、
ボタンを押しても反応しない
スクロールがカクカクする
入力しても文字がすぐ出てこない
こういう「イラッとする体験」になります。
非同期処理のパフォーマンス最適化でいう「UI ブロック回避」は、
一言で言うと、
「メインスレッドを独占しないように、処理の仕方を工夫すること」
です。
まずは「何が UI をブロックするのか」を体感する
重い同期処理の例
ブラウザのコンソールで、次のコードを実行してみてください。
function blockUI() {
console.log("重い処理開始");
const start = performance.now();
let sum = 0;
// わざと重いループ
for (let i = 0; i < 1_000_000_000; i++) {
sum += i;
}
const end = performance.now();
console.log("重い処理終了:", Math.round(end - start), "ms", sum);
}
blockUI();
JavaScript実行中、
タブが「応答なし」っぽくなったり、
スクロールやクリックが効かなくなったりするはずです。
理由はシンプルで、
メインスレッドがこの for ループで埋まっている
→ その間、イベント(クリック・キー入力)も描画も処理できない
からです。
ここが最初の重要ポイントです。
「UI ブロックの正体は、“メインスレッドを長時間占有する同期処理”」
非同期処理の話をする前に、まずこれを腹に落としておく必要があります。
非同期処理は「UI ブロックを避けるための道具」でもある
await 自体は UI を止めない
よくある誤解として、
「await があると止まるんじゃないの?」というものがあります。
例えば、次のコード。
async function loadData() {
const res = await fetch("/api/data");
const data = await res.json();
console.log(data);
}
JavaScriptここで await している間、
JavaScript のメインスレッドは「待ち続けている」わけではありません。
fetch はブラウザの外側の仕組みに仕事を渡し、
「レスポンスが返ってきたら教えてね」と登録して、いったんメインスレッドに戻る
という動きをします。
つまり、await の間も、
メインスレッドは他のイベント(クリック・描画など)を処理できます。
ここが重要です。
「UI をブロックするのは“重い同期処理”であって、“ネットワーク待ちの await”ではない。
むしろ await は、UI をブロックしないための仕組み側にいる。」
UI ブロックを避けるための基本戦略
戦略1:長時間処理を分割する
さっきのような重いループを、
「一気にやる」のではなく「少しずつ進める」ように書き換えます。
例として、10万件の配列を処理するケースを考えます。
同期で一気にやるとこうなります。
function heavyTask() {
const arr = Array.from({ length: 100000 }, (_, i) => i);
let sum = 0;
for (const n of arr) {
for (let i = 0; i < 1000; i++) {
sum += n * i;
}
}
console.log("完了", sum);
}
JavaScriptこれを「分割版」にするとこうなります。
function heavyTaskChunked() {
const arr = Array.from({ length: 100000 }, (_, i) => i);
const chunkSize = 500;
let index = 0;
let sum = 0;
console.log("処理開始");
function processChunk() {
const end = Math.min(index + chunkSize, arr.length);
for (; index < end; index++) {
for (let i = 0; i < 1000; i++) {
sum += arr[index] * i;
}
}
if (index < arr.length) {
setTimeout(processChunk, 0); // 一旦メインスレッドを解放
} else {
console.log("処理終了", sum);
}
}
processChunk();
}
heavyTaskChunked();
JavaScriptsetTimeout(processChunk, 0) がポイントです。
これは、
「今の処理が一段落したら、次のタイミングでまた processChunk を呼んでね」
という意味になります。
その「一段落」の間に、
ブラウザはクリックや描画を処理できます。
ここが重要です。
「長時間処理を分割して、間に“休憩”を挟むことで、UI に呼吸させる」
これが UI ブロック回避の王道テクニックです。
戦略2:requestAnimationFrame で「描画タイミングに合わせて進める」
アニメーションやスクロールと一緒に重い処理をしたいときは、requestAnimationFrame が相性抜群です。
function heavyTaskWithRAF() {
const arr = Array.from({ length: 100000 }, (_, i) => i);
const chunkSize = 1000;
let index = 0;
let sum = 0;
console.log("処理開始");
function processChunk() {
const end = Math.min(index + chunkSize, arr.length);
for (; index < end; index++) {
for (let i = 0; i < 500; i++) {
sum += arr[index] * i;
}
}
if (index < arr.length) {
requestAnimationFrame(processChunk); // 次のフレームで続き
} else {
console.log("処理終了", sum);
}
}
requestAnimationFrame(processChunk);
}
heavyTaskWithRAF();
JavaScriptrequestAnimationFrame は、
「次の画面描画の直前」に呼ばれます。
つまり、
1フレームごとに少しずつ処理を進める
→ その合間にブラウザが描画を挟める
→ アニメーションやスクロールが滑らかに保たれる
という流れになります。
UI ブロック回避という観点では、
「1フレームでやる仕事を増やしすぎない」
という意識がとても大事です。
戦略3:本当に重い処理は Web Worker に逃がす(概念だけ)
ここまでの話は「メインスレッドの中での工夫」でしたが、
それでも足りないくらい重い処理(画像処理・圧縮・巨大データ解析など)は、
そもそもメインスレッドでやるべきではありません。
そこで出てくるのが Web Worker です。
Worker は「別スレッド」で動く JavaScript で、
メインスレッドとはメッセージでやり取りします。
重い処理を Worker に任せて、
メインスレッドは UI のことだけ考える、という分業ができます。
初心者の段階では、
「UI をブロックするような処理は、いずれ Worker に逃がす選択肢がある」
くらいを知っておけば十分です。
まずは、
メインスレッド内での「分割」と「休憩」をしっかり使えるようになるのが先です。
非同期処理と UI ブロック回避の関係を整理する
「非同期だから安全」ではない
ここで一つ、よくある落とし穴をはっきりさせておきます。
async function heavy() {
let sum = 0;
for (let i = 0; i < 1_000_000_000; i++) {
sum += i;
}
return sum;
}
JavaScriptこの関数は async ですが、
中身は完全に同期処理です。
await heavy() と書いても、
中の for ループが終わるまでは UI がブロックされます。
async / await は「Promise を扱いやすくする構文」であって、
「中身を勝手に非同期にしてくれる魔法」ではありません。
ここが超重要です。
「UI ブロックを避けるには、“非同期っぽく書く”ことではなく、“重い処理を分割してメインスレッドを解放する”ことが本質」
という感覚を持ってください。
「待ち時間」と「ブロック時間」を分けて考える
非同期処理には、
「ネットワークを待っている時間」や
「ディスク I/O を待っている時間」など、
どうしても発生する待ち時間があります。
でも、その待ち時間は
メインスレッドをブロックする必要はない
というのがポイントです。
だから、
ネットワーク待ちは await で外に逃がす
CPU を使う重い処理は、チャンクに分けて合間に休憩を入れる
という二段構えで考えると、
UI ブロックをかなり避けられるようになります。
初心者として「UI ブロック回避」で本当に押さえてほしいこと
最後に、エッセンスをぎゅっとまとめます。
UI をブロックする犯人は「重い同期処理」
非同期処理(await)は、むしろ UI ブロックを避けるための仕組み側にいる
長時間処理は「チャンクに分けて、間に休憩を挟む」ことでメインスレッドを解放するasync と書いただけでは処理は軽くならない。中身が同期なら普通に固まる
おすすめの練習は、次の二つを自分の手で試すことです。
重い for ループをそのまま実行して、UI が固まる感覚を味わう
同じ処理を「チャンク+setTimeout(…, 0)」で分割して、固まり方の違いを体感する
一度その違いを身体で感じると、
あなたの中で「UI をブロックするコード」に対する警報が鳴るようになります。
そこまで来たら、もう立派に
「動くだけじゃなく、気持ちよく動くコードを書こうとしているエンジニア」
です。
