「スケジューリング設計」は「いつ・どのくらい・どう止めるか」を決めること
タイマーは「ちょっと遅らせて実行する便利関数」ではなく、
「処理のタイミングを設計するための道具」 です。
いつ実行するか
どのくらいの頻度で実行するか
どのくらいの精度が必要か
いつ止めるか
UI や他の非同期処理とどう噛み合わせるか
これを考えずに setTimeout や setInterval を置き始めると、
「なんかたまにバグる」「たまに二重実行される」「止まらない」みたいな、
じわじわ効いてくる不具合の温床になります。
ここでは、タイマーを「設計の道具」として扱う視点で整理していきます。
まず「何をしたいのか」で API を選ぶ
一回だけ遅らせたいのか、定期的に監視したいのか
一度だけ「あとで」実行したいなら setTimeout。
定期的に同じ処理をしたいなら setInterval か、setTimeout の再帰。
ただし、設計としてはこう考えるとよいです。
一回だけの遅延実行
→ setTimeout が素直
定期的な処理
→ 原則として「再帰 setTimeout」を第一候補にする
→ setInterval は「処理が軽くて、多少ズレてもいいとき」に限定する
再帰 setTimeout のイメージはこれです。
function loop() {
doSomething();
setTimeout(loop, 1000);
}
loop();
JavaScriptこれだと、
処理が終わってから 1 秒待つ
→ 次の実行
という流れになるので、
処理が多少重くても「詰まりにくい」設計になります。
UI を滑らかに動かしたいのか、裏方の処理なのか
UI を動かす・アニメーションするなら requestAnimationFrame。
裏方で「今すぐじゃなくていい処理」なら requestIdleCallback。
UI に関係するもの
→ requestAnimationFrame(描画タイミングと同期)
ログ送信や事前計算など裏方
→ requestIdleCallback(ブラウザがヒマなとき)
「ユーザーが今見ているもの」か
「ユーザーが気づかなくていい処理」かで、
使う API を切り替えるのがスケジューリング設計の第一歩です。
「止める設計」を最初から組み込む
タイマーは「開始」と「終了」をセットで設計する
タイマーを設計するときに、
一番やってはいけないのは「止め方を考えないこと」です。
例えば、ポーリング(定期的に API を叩く)を考えます。
setInterval(() => {
fetch("/api/status");
}, 5000);
JavaScriptこれだけだと、
ページを離れても
コンポーネントが破棄されても
永遠にリクエストを送り続けます。
設計としては、必ず「ライフサイクル」とセットにします。
let timerId = null;
function startPolling() {
if (timerId !== null) return;
timerId = setInterval(() => {
fetch("/api/status");
}, 5000);
}
function stopPolling() {
if (timerId !== null) {
clearInterval(timerId);
timerId = null;
}
}
JavaScript「いつ始めるか」「いつ終わるか」を関数として切り出しておくと、
画面のマウント・アンマウント、イベントの開始・終了などに
きれいに紐づけられます。
終了条件をコードに埋め込む
「どんな条件で終わるべきか」を、
日本語で言えるようにしてからコードに落とします。
例えば「最大 3 回までリトライするポーリング」。
let retryCount = 0;
const maxRetry = 3;
function poll() {
fetch("/api/status")
.then((res) => {
if (!res.ok) throw new Error("error");
return res.json();
})
.then((data) => {
console.log("成功", data);
})
.catch(() => {
retryCount++;
if (retryCount <= maxRetry) {
setTimeout(poll, 1000);
} else {
console.log("リトライ上限に達したので終了");
}
});
}
poll();
JavaScriptここでは、
成功したら終わる
失敗したら 1 秒後に再試行
ただし 3 回まで
という「終わり方」が明示されています。
スケジューリング設計では、
「どう始めるか」と同じくらい「どう終わるか」をコードに書くことが重要です。
精度と負荷をどうバランスさせるか
「ぴったり◯秒ごと」は基本的に無理だと知っておく
setTimeout(fn, 1000) と書いても、
きっちり 1000ms 後に実行されるとは限りません。
他の処理が重い
ブラウザがバックグラウンド
OS の制限
などで、平気でズレます。
「正確な時間」が必要なときは、
タイマーに頼らず「時計」を使います。
const start = Date.now();
function tick() {
const elapsed = Date.now() - start;
const seconds = Math.floor(elapsed / 1000);
console.log(`${seconds} 秒経過`);
if (seconds < 10) {
setTimeout(tick, 200);
} else {
console.log("10秒経ったので終了");
}
}
tick();
JavaScriptここでは、
タイマーは「更新のきっかけ」
正確な時間は Date.now() で計算
という役割分担になっています。
負荷を下げるために「間隔」と「分割」を設計する
例えば、重い処理を 1 万件分やりたいとき、
一気にループすると UI が固まります。
スケジューリング設計としては、
「小分けにして、タイマーで分散する」という発想を取ります。
const items = Array.from({ length: 10000 }, (_, i) => i);
let index = 0;
function workChunk() {
const start = Date.now();
while (index < items.length && Date.now() - start < 10) {
const item = items[index];
// item を処理
index++;
}
if (index < items.length) {
setTimeout(workChunk, 0);
} else {
console.log("完了");
}
}
workChunk();
JavaScriptここでは、
1 回の処理時間を 10ms 以内に抑える
残りは次のタイミングに回す
という「負荷の上限」を設計しています。
requestIdleCallback を使えば、
「ブラウザがヒマなときにだけ進める」という設計もできます。
非同期処理(Promise / async/await)との組み合わせ方
タイマーを Promise 化して「待てるもの」にする
setTimeout はコールバック型ですが、
設計としては Promise に包んでしまうと扱いやすくなります。
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function run() {
console.log("1");
await wait(1000);
console.log("2");
await wait(1000);
console.log("3");
}
run();
JavaScriptこうすると、
処理の流れが読みやすい
他の非同期処理(fetch など)と同じ文脈で扱える
テストもしやすい
というメリットがあります。
スケジューリング設計の観点では、
「タイミング制御を async/await の世界に持ち込む」ことで、
コード全体の見通しが良くなります。
「待ち時間」も設計の一部にする(バックオフ戦略)
API のリトライなどでは、
毎回同じ間隔で叩くのではなく、
徐々に間隔を伸ばす「エクスポネンシャルバックオフ」がよく使われます。
async function fetchWithRetry(url, maxRetry = 5) {
let retry = 0;
let delay = 500;
while (retry <= maxRetry) {
try {
const res = await fetch(url);
if (!res.ok) throw new Error("HTTP error");
return await res.json();
} catch (e) {
if (retry === maxRetry) throw e;
await wait(delay);
delay *= 2;
retry++;
}
}
}
JavaScriptここでは、
失敗するたびに待ち時間を伸ばす
→ サーバーへの負荷を抑える
→ ネットワークの一時的な不調にも優しい
という「時間の設計」をしています。
スケジューリング設計は、
「どのくらいの頻度で外部と話すか」を決めることでもあります。
UI とスケジューリングを一緒に設計する
UI 更新は requestAnimationFrame、裏方は setTimeout / requestIdleCallback
例えば、チャットアプリの新着メッセージ表示を考えます。
新着を取りに行くポーリング
→ setTimeout の再帰で数秒ごとに実行
→ バックオフ戦略を入れてもよい
メッセージのフェードインやスクロールアニメーション
→ requestAnimationFrame で描画タイミングに合わせる
ログ送信や解析
→ requestIdleCallback でヒマなときに送る
このように、
「ユーザーが体感する部分」
「裏でこっそりやる部分」
を分けて、それぞれに合ったスケジューリングを選ぶのが、
UI を気持ちよく保つ設計です。
無限ループ・二重起動を防ぐための状態管理
タイマー設計でよくある事故が、
同じタイマーを何度も開始してしまう
止めるべきタイミングで止めていない
というものです。
これを防ぐには、
「状態」を変数として持つのが基本です。
let timerId = null;
let running = false;
function start() {
if (running) return;
running = true;
timerId = setInterval(() => {
console.log("tick");
}, 1000);
}
function stop() {
if (!running) return;
running = false;
clearInterval(timerId);
timerId = null;
}
JavaScript「今動いているかどうか」をrunning や timerId で表現しておくと、
スケジューリングのバグが一気に減ります。
まとめとしての「スケジューリング設計のチェックリスト的な感覚」
一回だけか、繰り返しか
→ setTimeout か、再帰 setTimeout か、setInterval か
UI に関係するか、裏方か
→ requestAnimationFrame か、requestIdleCallback か
いつ始めて、いつ終わるか
→ 開始・終了関数を用意して、ライフサイクルに結びつける
どのくらいの精度が必要か
→ タイマーに頼るのか、Date.now() / performance.now() で補正するのか
負荷をどう抑えるか
→ 間隔・分割・バックオフを設計する
ここまで意識してタイマーを書くようになると、
「とりあえず setTimeout 置いとくか」という世界から一段抜けて、
「時間を設計できるプログラマ」 になっていきます。
もしよければ、今書いているコードの中で
「なんとなく setInterval を置いているところ」
「止め方を考えていないタイマー」
を一つ見つけて、
今日の話をもとに設計し直してみてください。
そこから一気に感覚が変わります。

