UI 更新とタイマーの関係は「JavaScript とブラウザの役割分担」を理解すると一気に分かりやすくなる
タイマー(setTimeout / setInterval / requestAnimationFrame)を使うとき、
初心者がよくつまずくのが 「UI が更新されない」「画面が固まる」 といった現象です。
これは、
JavaScript が UI を直接描画しているわけではない
という前提を理解するとスッキリします。
JavaScript は「UI をこう変えて」とブラウザに指示するだけで、
実際の描画はブラウザが「描画タイミング」に合わせて行います。
この「描画タイミング」と「タイマーの動き」がズレると、
UI が更新されないように見えたり、カクついたりするわけです。
JavaScript とブラウザの「描画サイクル」を理解する
JavaScript は UI を“即座に”変えているわけではない
例えば、こう書いたとします。
box.style.left = "100px";
JavaScriptこれは「左に 100px 動かして」とブラウザに依頼しているだけで、
その瞬間に画面が描き変わるわけではありません。
ブラウザは、
JavaScript の処理が終わる
→ レイアウト計算
→ ペイント
→ 実際の画面に反映
という流れで描画します。
つまり、
JavaScript の処理が長引くと、UI の更新も遅れる
ということです。
setTimeout / setInterval は「描画タイミングを知らない」
setTimeout や setInterval は、
「◯ミリ秒後に実行してね」
というだけで、
ブラウザの描画タイミングとは無関係 です。
そのため、
100ms ごとに UI を更新したい
→ でも描画は 16ms ごと(60fps)
→ タイミングがズレてカクつく
という現象が起きます。
requestAnimationFrame は「描画直前に呼ばれる」
一方、requestAnimationFrame は、
「次の描画直前に呼んでね」
という API です。
だから、
UI を滑らかに動かしたい
→ requestAnimationFrame が最適
という構造になっています。
UI が更新されない典型例とその理由
例:重い処理が UI 更新をブロックする
box.textContent = "更新中…";
setTimeout(() => {
const start = Date.now();
while (Date.now() - start < 2000) {
// 2秒間ブロックする重い処理
}
box.textContent = "完了!";
}, 0);
JavaScript一見、
「すぐ“更新中…”が表示されて、2 秒後に“完了!”になる」
と思いがちですが、実際には、
“更新中…”が表示されないまま 2 秒固まる → “完了!”が表示される
という動きになります。
理由は、
setTimeout(..., 0) が「すぐ実行」ではなく「後回し」
その後の while が 2 秒間メインスレッドを占有
→ ブラウザが描画できない
→ “更新中…” が反映されない
という流れです。
つまり、
UI 更新は JavaScript が止まっている間は絶対に起きない
ということです。
UI 更新とタイマーを正しく連携させる方法
UI を更新したい → requestAnimationFrame を使う
アニメーションや連続した UI 更新は、requestAnimationFrame が最適です。
let x = 0;
function move() {
x += 2;
box.style.left = `${x}px`;
if (x < 300) {
requestAnimationFrame(move);
}
}
requestAnimationFrame(move);
JavaScriptこれなら、
描画タイミングに合わせて更新
→ カクつきが少ない
→ CPU の無駄も少ない
という理想的な動きになります。
重い処理は UI 更新と同じタイミングでやらない
重い処理を UI 更新と同じタイミングでやると、
UI が固まります。
そこで使えるのが、
setTimeout(分割実行)requestIdleCallback(ブラウザがヒマなときに実行)
Web Worker(別スレッドで実行)
などです。
例えば、重い処理を小分けにするなら:
function heavyWorkChunk() {
for (let i = 0; i < 1000; i++) {
// 小さな処理
}
if (まだ残りがある) {
setTimeout(heavyWorkChunk, 0); // UI をブロックしない
}
}
heavyWorkChunk();
JavaScriptこうすると、
UI 更新のタイミングを奪わずに重い処理を進められます。
タイマーと UI 更新の「ズレ」を理解する
setInterval は UI 更新とズレやすい
setInterval(() => {
box.style.left = `${x}px`;
x += 2;
}, 16);
JavaScript16ms は 60fps の理論値ですが、
実際には、
処理が重い
ブラウザの描画タイミングとズレる
バックグラウンドで間引かれる
などの理由で、
カクつきやズレが発生します。
だから、アニメーションには向きません。
setTimeout も「UI 更新の直前に実行される」とは限らない
setTimeout(() => {
box.style.left = "100px";
}, 1000);
JavaScriptこれは「1 秒後に UI を更新する」だけで、
描画タイミングに合わせてくれるわけではありません。
そのため、
連続した UI 更新には不向きです。
UI 更新を滑らかにするための実践的な考え方
UI を変える処理は「短く」「軽く」
UI 更新の直前に重い処理があると、
描画が遅れます。
UI を更新する関数は、
DOM 操作は最小限
計算は軽く
重い処理は別のタイミングに回す
という設計が重要です。
UI 更新のタイミングは「ブラウザが決める」
JavaScript が「今すぐ更新して」と言っても、
実際に描画されるのはブラウザのタイミングです。
だから、
UI を滑らかにしたい
→ requestAnimationFrame
UI を固めたくない
→ 重い処理は分割 or idle 時に実行
という役割分担が必要です。
初心者として「UI 更新とタイマーの関係」で本当に掴んでほしいこと
UI は JavaScript が直接描くのではなく、ブラウザが描画タイミングで反映する
setTimeout / setInterval は描画タイミングを知らないので、UI 更新がズレたりカクついたりする
重い処理があると UI 更新がブロックされる(JavaScript はシングルスレッド)
アニメーションや連続 UI 更新は requestAnimationFrame が最適
重い処理は分割するか、idle 時に回すか、Web Worker に逃がす
まずは、
setInterval でアニメーション
→ requestAnimationFrame に書き換える
という小さな実験をしてみると、
UI 更新とタイマーの関係が一気に腑に落ちるはずです。
