- JavaScript
JS setTimeout・setIntervalの使い方|sleepと停止
setTimeoutは「指定した時間のあとに1回だけ関数を実行する」関数、setIntervalは「指定した間隔で繰り返し実行する」関数です。 止めるときは、それぞれの戻り値を clearTimeout と clearInterval に渡します。JavaScriptには sleep 関数が用意されていませんが、setTimeoutをPromiseで包めば await sleep(1000) と書けるようになります。
この記事では、基本の書き方から、async/awaitで使うsleep、処理が重なるsetIntervalの再現と直し方、Node.jsの timers/promises、AbortSignalでのキャンセル、ReactのuseEffectでのクリーンアップ、this が外れるエラー、最小遅延4msと上限2^31-1までを扱います。コードはNode.js 22.22.2とChromium 141(Playwright 1.63経由)で実行し、出力をそのまま載せています。 非アクティブタブでの間引きだけは手元で再現できなかったため、MDNの記載をもとに整理しています。
setTimeout・setIntervalの早見表
| やりたいこと | 書き方 |
|---|---|
| 1秒後に1回実行する | const id = setTimeout(fn, 1000) |
| 予約を取り消す | clearTimeout(id) |
| 1秒ごとに繰り返す | const id = setInterval(fn, 1000) |
| 繰り返しを止める | clearInterval(id) |
| 関数に引数を渡す | setTimeout(fn, 1000, "a", "b") |
| 1秒待つ(sleep) | await new Promise((r) => setTimeout(r, 1000)) |
| 1秒待つ(Node.js) | import { setTimeout as sleep } from "node:timers/promises" → await sleep(1000) |
| 待ちを途中でやめる | await sleep(5000, null, { signal }) と AbortController |
| 処理が重ならない繰り返し | 処理の最後で次の setTimeout を予約する(入れ子setTimeout) |
| 今の処理が終わってから実行 | setTimeout(fn, 0) |
| Reactで使う | useEffect の中で開始し、return () => clearInterval(id) で止める |
2つの関数の違いは次のとおりです。
| 項目 | setTimeout | setInterval |
|---|---|---|
| 実行回数 | 1回 | 止めるまで繰り返す |
| 止める関数 | clearTimeout(id) |
clearInterval(id) |
| 戻り値(ブラウザ) | 数値のID | 数値のID |
| 戻り値(Node.js) | Timeout オブジェクト |
Timeout オブジェクト |
| 処理時間が間隔より長いとき | 影響なし | 次の実行が詰まる・重なる |
| 向いている用途 | 遅らせて1回、sleep、処理が重ならない繰り返し | 時計の表示など軽い処理の定期実行 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「JS setTimeout・setIntervalの使い方|sleepと停止」
setTimeoutの使い方(1回だけ遅らせて実行)
基本形と引数の渡し方
// basic.mjs(Node.js 22.22.2で実行: node basic.mjs)
const t0 = performance.now();
const ms = () => String(Math.round(performance.now() - t0)).padStart(5) + "ms";
// 第3引数以降は、関数にそのまま渡される
const id = setTimeout((a, b) => console.log(ms(), "timeout", a, b), 100, "x", "y");
// 戻り値を clearTimeout に渡すと取り消せる
const cancelId = setTimeout(() => console.log("never"), 50);
clearTimeout(cancelId);
実行結果:
114ms timeout x y
"never" は表示されません。予約した時間が来る前に clearTimeout を呼べば、その関数は実行されません。
| 引数 | 意味 | 省略したとき |
|---|---|---|
| 第1引数 | 実行する関数 | 必須 |
| 第2引数 | 遅延(ミリ秒)。1000で1秒 | 0 |
| 第3引数以降 | 関数に渡す引数 | なし |
関数は「呼び出さずに」渡す
よくあるつまずきが、関数にかっこを付けて渡してしまう書き方です。
// かっこを付けると、その場で実行され、戻り値が setTimeout に渡る
setTimeout(showMessage(), 1000);
// 関数そのものを渡す
setTimeout(showMessage, 1000);
// 引数を付けたいときはアロー関数で包む
setTimeout(() => showMessage("こんにちは"), 1000);
showMessage() と書くと「今すぐ実行した結果」を渡すことになります。 1秒待たずに表示された場合は、かっこの有無を確認します。
戻り値はブラウザでは数値、Node.jsではオブジェクト
const id = setTimeout(() => {}, 100);
console.log(typeof id, id.constructor.name);
| 環境 | 出力 |
|---|---|
| Node.js 22.22.2 | object Timeout |
| Chromium 141 | number(正の整数) |
どちらもそのまま clearTimeout(id) に渡せます。TypeScriptで両方の環境に対応する型を書くなら、ReturnType<typeof setTimeout> としておくと環境の違いを吸収できます。
setTimeout(fn, 0) は「今の処理が終わったあと」
遅延に0を指定しても、すぐには実行されません。今実行中のコードと、Promiseの後続処理(マイクロタスク)がすべて終わってから実行されます。
setTimeout(() => console.log("timeout 0"), 0);
Promise.resolve().then(() => console.log("promise"));
console.log("sync");
実行結果(Node.js 22、Chromium 141とも同じ順番):
sync
promise
timeout 0
この順番の仕組みは「JavaScriptイベントループ」の記事で詳しく扱っています。ここでは 「setTimeoutの0は、Promiseより後」 とだけ押さえておけば十分です。
setIntervalの使い方(繰り返し実行と停止)
// 100ミリ秒ごとに実行し、3回で止める
let n = 0;
const iv = setInterval(() => {
n++;
console.log(ms(), "tick", n);
if (n === 3) clearInterval(iv); // コールバックの中から止められる
}, 100);
実行結果:
265ms tick 1
364ms tick 2
465ms tick 3
setIntervalは、止めない限りずっと動き続けます。 画面を離れたときや不要になったときに clearInterval を呼ぶことを、開始とセットで書く習慣にすると安心です。
| 止め方 | 書き方 |
|---|---|
| 条件を満たしたらコールバック内で止める | if (n === 3) clearInterval(iv) |
| 一定時間後に止める | setTimeout(() => clearInterval(iv), 5000) |
| ボタンで止める | stopBtn.addEventListener("click", () => clearInterval(iv)) |
| 二重に開始しない | 開始前に clearInterval(iv) を呼んでから iv = setInterval(...) |
最後の「二重に開始しない」は見落としやすい点です。開始ボタンを2回押すとタイマーが2本動き、IDを上書きした1本目は止められなくなります。開始の直前にいったん clearInterval を呼ぶと、何度押しても1本だけになります。
let timerId;
startBtn.addEventListener("click", () => {
clearInterval(timerId); // すでに動いていれば止める
timerId = setInterval(update, 1000);
});
stopBtn.addEventListener("click", () => clearInterval(timerId));
clearTimeout と clearInterval は、存在しないIDや undefined を渡してもエラーになりません。そのため、上のように「念のため先に止める」書き方ができます。
async/awaitで使うsleepの書き方
JavaScriptには sleep() が用意されていません。setTimeoutをPromiseで包むと、1行で作れます。
// sleep.mjs(Node.js 22.22.2で実行)
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
console.log(ms(), "start");
await sleep(300);
console.log(ms(), "after sleep(300)");
// ループの中で順番に待つ
for (const i of [1, 2, 3]) {
await sleep(100);
console.log(ms(), "item", i);
}
実行結果(抜粋):
465ms start
766ms after sleep(300)
1068ms item 1
1168ms item 2
1269ms item 3
await は async 関数の中か、モジュール(.mjs や type="module")のトップレベルで使えます。 通常のスクリプトで使うときは async function main() { ... } で包みます。
forEachの中のawaitは待ってくれない
sleepを使い始めると出会いやすいのが、forEach との組み合わせです。
| 書き方 | 動き |
|---|---|
items.forEach(async (x) => { await sleep(100); ... }) |
全件がほぼ同時に始まる(順番に待たない) |
for (const x of items) { await sleep(100); ... } |
1件ずつ順番に待つ |
await Promise.all(items.map(async (x) => { ... })) |
全件を同時に始めて、全部終わるまで待つ |
forEach はコールバックが返すPromiseを待ちません。順番に待ちたいときは for...of を使います。 複数の非同期処理をまとめて待つ方法は「Promise.all・allSettled・race・anyの違い」の記事で扱っています。
Node.jsなら timers/promises が使える
Node.jsには、Promiseを返すタイマーが標準で用意されています(Node.js 15で追加された node:timers/promises モジュールです)。
import { setTimeout as sleep, setInterval as every } from "node:timers/promises";
const v = await sleep(200, "value"); // 第2引数が解決値になる
console.log(v);
// 一定間隔の繰り返しを for await で書ける
let c = 0;
const s = performance.now();
for await (const _ of every(100)) {
c++;
console.log(Math.round(performance.now() - s), "iter", c);
if (c === 3) break; // break で止まる
}
実行結果:
value
101 iter 1
201 iter 2
301 iter 3
| 環境 | sleepの書き方 |
|---|---|
| ブラウザ | const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) |
| Node.js | import { setTimeout as sleep } from "node:timers/promises" |
| 両方で動かすコード | 上のPromise版(どちらでも動く) |
for await と break で書く繰り返しは、clearInterval を呼び忘れる心配がなく、ループを抜ければタイマーも片付きます。
AbortSignalで待ちを途中でやめる
「最大5秒待つが、ユーザーがキャンセルしたらすぐやめる」のような処理は、AbortController で書けます。timers/promises は signal オプションを受け取ります。
import { setTimeout as sleep } from "node:timers/promises";
const ac = new AbortController();
setTimeout(() => ac.abort(), 100); // 100ミリ秒後にキャンセル
try {
await sleep(5000, null, { signal: ac.signal });
} catch (e) {
console.log(e.name, e.code, e.message);
}
// 「一定時間で自動的に中断するsignal」も作れる
try {
await sleep(5000, null, { signal: AbortSignal.timeout(100) });
} catch (e) {
console.log(e.name, e.code, e.message);
}
実行結果(Node.js 22.22.2):
AbortError ABORT_ERR The operation was aborted
AbortError ABORT_ERR The operation was aborted
5秒待たずに、約100ミリ秒で AbortError が投げられています。
ブラウザでも使える、中断できるsleep
ブラウザには timers/promises がないので、自分で書きます。次の関数はNode.jsでもブラウザでも動きます。
function sleepAbortable(ms, signal) {
return new Promise((resolve, reject) => {
if (signal?.aborted) return reject(signal.reason); // すでに中断済み
const id = setTimeout(() => {
signal?.removeEventListener("abort", onAbort);
resolve();
}, ms);
const onAbort = () => {
clearTimeout(id); // タイマーを片付ける
reject(signal.reason);
};
signal?.addEventListener("abort", onAbort, { once: true });
});
}
const ac2 = new AbortController();
setTimeout(() => ac2.abort(), 100);
try {
await sleepAbortable(5000, ac2.signal);
} catch (e) {
console.log(e.name, e.message);
}
実行結果(Node.js 22.22.2):
AbortError This operation was aborted
ポイントは3つです。
- 中断されたら
clearTimeoutでタイマーを片付ける。 片付けないと、不要なタイマーが時間まで残ります。 - 正常に終わったら
removeEventListenerでリスナーを外す。 同じsignalを使い回すときに、リスナーがたまりません。 - 最初に
signal.abortedを確認する。 すでに中断済みのsignalを渡されたときに、すぐ終われます。
同じsignalを fetch(url, { signal }) にも渡せば、「通信も待ちもまとめてキャンセル」が1つのコントローラーでできます。
setIntervalで処理が重なる現象と、入れ子setTimeoutでの直し方
再現:処理時間が間隔より長いと重なる
setIntervalは、前回の処理が終わったかどうかに関係なく、次の実行を予約します。100ミリ秒間隔で、1回に250ミリ秒かかる非同期処理(通信を想定)を動かしてみます。
// overlap.mjs(Node.js 22.22.2で実行)
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const s = performance.now();
const r = () => Math.round(performance.now() - s);
let run = 0, active = 0;
const iv = setInterval(async () => {
const k = ++run;
active++;
console.log(r(), `start#${k} active=${active}`);
await sleep(250); // 250ミリ秒かかる処理
active--;
console.log(r(), `end#${k}`);
}, 100);
setTimeout(() => clearInterval(iv), 350);
実行結果:
101 start#1 active=1
200 start#2 active=2
301 start#3 active=3
351 end#1
450 end#2
551 end#3
1回目が終わる前に2回目、3回目が始まり、同時に3本が動いています(active=3)。定期的にサーバーへ問い合わせる処理でこれが起こると、応答が遅いときほどリクエストが積み重なります。
直した後:処理が終わってから次を予約する
処理の最後で次の setTimeout を予約する形にすると、重なりません。これを「入れ子のsetTimeout」や「再帰的なsetTimeout」と呼びます。
// nested.mjs(Node.js 22.22.2で実行)
let run = 0;
async function loop() {
const k = ++run;
console.log(r(), `start#${k}`);
await sleep(250);
console.log(r(), `end#${k}`);
if (k === 3) return; // 終了条件
setTimeout(loop, 100); // 終わってから次を予約
}
setTimeout(loop, 100);
実行結果:
101 start#1
351 end#1
451 start#2
706 end#2
806 start#3
1057 end#3
前の処理が終わってから、きっちり100ミリ秒あけて次が始まっています。 止めたいときは、次の setTimeout を予約しない(または戻り値を clearTimeout する)だけです。
同期の重い処理では「重なる」のではなく「詰まる」
処理が await を含まない同期処理の場合、JavaScriptは同時に2つの処理を動かせないので、重なりはしません。その代わり、間隔がなくなります。100ミリ秒間隔で、150ミリ秒かかる同期処理を動かした結果です。
101 start#1
251 end#1
251 start#2
401 end#2
401 start#3
551 end#3
1回目が終わった251ミリ秒の時点で、すぐに2回目が始まっています。休みなく処理が続くため、その間は画面の描画やクリックへの反応が待たされます。 この場合も入れ子setTimeoutにすれば、処理と処理の間に必ず100ミリ秒の空きができます。
時刻のずれを抑えたいときは、次の予定時刻から逆算する
入れ子setTimeoutは「処理の終わりから次の始まりまで」が一定になるので、処理時間のぶんだけ1周が長くなります。1回20ミリ秒かかる処理を100ミリ秒間隔で10回動かし、開始時刻(ミリ秒)を比べました。
// 予定時刻から逆算して、次の遅延を決める
const start = performance.now();
let i = 0;
function tick() {
doWork(); // 約20ミリ秒かかる処理
i++;
const next = start + (i + 1) * 100; // 次に実行したい時刻
setTimeout(tick, Math.max(0, next - performance.now()));
}
setTimeout(tick, 100);
実行結果(Node.js 22.22.2、各回の開始時刻):
入れ子(固定100ms) 101 222 342 466 588 708 828 949 1072 1193
入れ子(逆算) 101 200 299 400 500 600 699 800 899 1000
setInterval 101 202 302 403 504 605 704 804 904 1004
| 方式 | 10回目の開始 | 特徴 |
|---|---|---|
| 入れ子setTimeout(固定の遅延) | 1193 ms | 処理が重ならない。1周が「処理時間+遅延」になる |
| 入れ子setTimeout(予定時刻から逆算) | 1000 ms | 重ならず、時刻のずれも積み上がらない |
| setInterval | 1004 ms | 書き方が短い。処理が長いと詰まる・重なる |
処理が軽ければsetIntervalで十分、処理時間が読めないなら入れ子setTimeout、時刻を合わせたいなら逆算つきの入れ子setTimeout、と選び分けられます。
なお、時計やカウントダウンのように「正しい時刻を表示する」ことが目的なら、タイマーが呼ばれた回数を数えるのではなく、呼ばれるたびに Date.now() で現在時刻を取り直して計算するのが確実です。タイマーは後述のとおり、タブの状態によって間引かれることがあるためです。
| 方式を選ぶ基準 | 選ぶもの |
|---|---|
| 時計の表示など、軽い処理を定期的に | setInterval |
| 通信など、処理時間が読めないものを定期的に | 入れ子setTimeout |
| 失敗したら間隔を広げたい | 入れ子setTimeout(次の遅延を毎回決められる) |
| 入力が止まってから1回だけ | debounce(「JavaScript debounce・throttle」の記事を参照) |
| アニメーションを描く | requestAnimationFrame |
ReactのuseEffectでタイマーを片付ける
Reactのコンポーネントでタイマーを使うときは、useEffect の中で開始し、クリーンアップ関数で止めます。
import { useEffect, useState } from "react";
function Clock() {
const [sec, setSec] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setSec((s) => s + 1); // 前の値をもとに更新する
}, 1000);
return () => clearInterval(id); // クリーンアップ:画面から消えるときに止める
}, []);
return <p>{sec}</p>;
}
クリーンアップの有無で何が変わるかを、React 19.3.0とChromium 141で確かめました(間隔は100ミリ秒、250ミリ秒後にコンポーネントを画面から外し、開始・実行・停止をログに記録)。
| 条件 | ログ | わかること |
|---|---|---|
| クリーンアップなし | start id=1 / tick / tick / tick / tick |
画面から外したあともtickが続く |
| クリーンアップあり | start id=1 / tick / tick / clear id=1 |
外した時点で止まる |
| クリーンアップあり+StrictMode | start id=1 / clear id=1 / start id=2 / tick / tick / clear id=2 |
開始→停止→再開始の順で1本だけ残る |
| クリーンアップなし+StrictMode | start id=1 / start id=2 / tick id=1 / tick id=2 / ... |
タイマーが2本動く |
開発時の StrictMode は、クリーンアップの書き忘れに気づけるよう、エフェクトを「実行→クリーンアップ→もう一度実行」します。クリーンアップを書いていないと、最下行のようにタイマーが2本動き、カウントが2倍の速さで進みます。 「開発中だけ2回動く」と感じたら、クリーンアップ関数を確認するのが近道です。
setSec((s) => s + 1) と関数を渡しているのもポイントです。setSec(sec + 1) と書くと、エフェクトが最初に見た sec(0)を使い続けるため、表示が1から進みません。
| Reactでの書き方 | ポイント |
|---|---|
| setInterval | return () => clearInterval(id) |
| setTimeout | return () => clearTimeout(id) |
| 値をもとに更新 | setState((prev) => ...) の形にする |
| 間隔を変えたい | 依存配列に間隔の値を入れる(変わるたびに止めて再開始される) |
thisが外れるエラーの再現と直し方
クラスのメソッドをそのままsetTimeoutに渡すと、this が期待した値になりません。
// this.mjs(Node.js 22.22.2で実行)
class Cart {
items = [];
add() {
this.items.push("pen");
console.log("items =", this.items.length);
}
}
const cart = new Cart();
setTimeout(cart.add, 10); // メソッドだけを取り出して渡している
実行結果(Node.js 22.22.2):
TypeError: Cannot read properties of undefined (reading 'push')
Chromium 141でも同じ内容で、コンソールには Uncaught TypeError: Cannot read properties of undefined (reading 'push') と表示されました。
cart.add と書いた時点で、メソッドは cart から切り離された「ただの関数」になります。setTimeoutがそれを呼ぶとき、this はブラウザでは window、Node.jsでは Timeout オブジェクトになり、どちらにも items がないので undefined の push を読もうとしてエラーになります。
直し方は2つです。
setTimeout(() => cart.add(), 10); // アロー関数で包む
setTimeout(cart.add.bind(cart), 20); // bind で this を固定する
実行結果:
items = 1
items = 2
| 書き方 | this | 結果 |
|---|---|---|
setTimeout(cart.add, 10) |
window または Timeout |
TypeError |
setTimeout(() => cart.add(), 10) |
cart |
動く |
setTimeout(cart.add.bind(cart), 10) |
cart |
動く |
迷ったらアロー関数で包む、と覚えておけば十分です。this.items.push のようなプロパティ参照ではなく this.count++ のような計算だと、エラーにならずに NaN になることもあります(Node.js 22で確認)。エラーが出ないぶん気づきにくいので、メソッドを渡すときは形で判断するのがおすすめです。
遅延時間の仕様:最小4msと最大2^31-1
入れ子が5回を超えると最小4ミリ秒になる(ブラウザ)
HTML標準では、setTimeoutの入れ子が5回を超えると、遅延が4ミリ秒未満の場合は4ミリ秒に引き上げると定められています。遅延0の入れ子setTimeoutを10回実行し、各回の時刻を記録しました。
const s = performance.now();
const out = [];
let i = 0;
function f() {
out.push((performance.now() - s).toFixed(1));
if (++i === 10) return console.log(out.join(" "));
setTimeout(f, 0);
}
setTimeout(f, 0);
実行結果(Chromium 141):
0.1 0.3 0.3 0.3 0.3 0.3 4.5 8.6 12.7 16.8
最初の数回はほぼ0ミリ秒で、途中から約4ミリ秒ずつ増えています。同じコードをNode.js 22.22.2で8回実行すると、次のようになりました。
1.3 2.4 4.2 5.3 6.4 7.5 8.6 9.7
Node.jsには4ミリ秒の引き上げはなく、代わりに1ミリ秒未満の遅延は1ミリ秒として扱われるため、約1ミリ秒ずつ進みます。
| 環境 | 遅延0を指定したとき |
|---|---|
| ブラウザ(入れ子5回まで) | ほぼ0ミリ秒 |
| ブラウザ(入れ子が5回を超えたあと) | 最小4ミリ秒 |
| Node.js | 最小1ミリ秒 |
いずれにしても、指定した遅延は「最短でこの時間」であって、ぴったりその時間に実行される保証ではありません。 他の処理が動いていれば、そのぶん後ろにずれます。
2^31-1ミリ秒(約24.8日)を超えると、すぐ実行される
遅延は符号つき32ビット整数として扱われるため、上限は2,147,483,647ミリ秒(約24.8日)です。これを超える値を渡すと、長く待つのではなく、すぐに実行されます。
const s = performance.now();
setTimeout(() => console.log("2^31 fired after", Math.round(performance.now() - s), "ms"), 2 ** 31);
setTimeout(() => console.log("25days fired after", Math.round(performance.now() - s), "ms"), 25 * 24 * 60 * 60 * 1000);
実行結果(Node.js 22.22.2):
(node:5969) TimeoutOverflowWarning: 2147483648 does not fit into a 32-bit signed integer.
Timeout duration was set to 1.
(node:5969) TimeoutOverflowWarning: 2160000000 does not fit into a 32-bit signed integer.
Timeout duration was set to 1.
2^31 fired after 1 ms
25days fired after 2 ms
Chromium 141では警告は出ず、どちらも0ミリ秒で実行されました。上限ちょうどの 2 ** 31 - 1 は、300ミリ秒待っても実行されないことを確認しています。
| 指定した遅延 | Node.js 22 | Chromium 141 |
|---|---|---|
2 ** 31 - 1(上限ちょうど) |
予約される | 予約される |
2 ** 31 |
警告を出して1ミリ秒後に実行 | 0ミリ秒で実行 |
| 25日ぶんのミリ秒 | 警告を出して1ミリ秒後に実行 | 0ミリ秒で実行 |
「30日後に実行」のような長い予約は、setTimeoutではなく、実行予定の日時を保存しておき、短い間隔のチェックやサーバー側のスケジューラーで判定する形にします。ページを閉じればタイマーは消えるので、長期間の予約にはそもそも保存の仕組みが必要です。
非アクティブタブではタイマーが間引かれる(MDNで確認)
ブラウザは、表示されていないタブのタイマーを間引いて、電力とCPUの消費を抑えます。この動作はPlaywrightのヘッドレスChromiumでは再現できなかった(別タブを前面にしても document.visibilityState が visible のままで、100ミリ秒間隔が保たれた)ため、以下はMDNの setTimeout() のページで確認した内容です。
| ブラウザ・状態 | タイマーの扱い(MDNの記載) |
|---|---|
| Firefox(デスクトップ)の非アクティブタブ | 最小1秒 |
| Firefox for Android の非アクティブタブ | 最小15分。タブごと解放されることもある |
| Chrome:非表示になって5分未満など | 間引きあり |
| Chrome 88以降:入れ子5回以上、非表示が5分超、無音が30秒超、WebRTC未使用 | 集中的な間引き。タイマーの確認は1分に1回 |
| 音を再生している、WebRTCを使っているなど | 間引きが緩和される |
対応の考え方は次のとおりです。
- 経過時間は回数ではなく時刻の差で求める。
Date.now()やperformance.now()の差を使えば、間引かれても表示は正しくなります。 - タブに戻ってきたタイミングで更新する。
document.addEventListener("visibilitychange", ...)でdocument.visibilityState === "visible"を検知し、その時点で最新の状態を取り直します。 - 裏で動き続ける必要がある処理は、サーバー側やWeb Workerなど別の仕組みを検討する。
つまずきやすいポイントのチェックリスト
| 症状 | 原因 | 直し方 |
|---|---|---|
| 待たずにすぐ実行される | setTimeout(fn(), 1000) とかっこを付けた |
setTimeout(fn, 1000) か () => fn() |
| 待たずにすぐ実行される(その2) | 遅延が2^31-1を超えている | 日時を保存して短い間隔で判定する |
Cannot read properties of undefined |
メソッドをそのまま渡して this が外れた |
アロー関数で包む、または bind |
| 止めたのに動き続ける | IDを上書きして1本目が残っている | 開始前に clearInterval を呼ぶ |
| 開発中だけ2倍速で動く(React) | useEffectのクリーンアップがない | return () => clearInterval(id) |
| カウントが1から進まない(React) | setSec(sec + 1) が最初の値を見続けている |
setSec((s) => s + 1) |
| リクエストが積み重なる | setIntervalで時間のかかる非同期処理 | 入れ子setTimeoutに変える |
| ループで順番に待ってくれない | forEach の中で await |
for...of に変える |
| タブを裏にすると遅くなる | 非アクティブタブの間引き | 時刻の差で計算し、visibilitychange で更新 |
| Node.jsのプログラムが終了しない | タイマーが残っている | clearInterval するか、timer.unref() を呼ぶ |
最後の unref() はNode.js固有の機能です。setTimeout の戻り値に対して unref() を呼ぶと、「このタイマーだけが残っている場合はプロセスを終了してよい」という指定になります(Node.js 22で hasRef() が false になり、60秒のタイマーを残したままプロセスが終了することを確認しました)。
よくある質問
Q. JavaScriptにsleep関数はありますか?
A. 標準の sleep() はありませんが、const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) の1行で作れます。async 関数の中で await sleep(1000) と書けば1秒待てます。Node.jsなら import { setTimeout as sleep } from "node:timers/promises" で、標準のPromise版タイマーが使えます。
Q. setIntervalと入れ子のsetTimeoutはどちらを使えばよいですか? A. 時計の表示のように処理が軽いならsetIntervalが短く書けます。通信のように処理時間が読めないものは、処理の最後で次のsetTimeoutを予約する入れ子の形にすると、処理が重なりません。今回の計測では、100ミリ秒間隔で250ミリ秒かかる処理をsetIntervalで動かすと、同時に3本が動きました。
Q. clearTimeoutとclearIntervalは入れ替えて使えますか?
A. 仕様上、タイマーのIDは共通の管理になっているため、入れ替えても止まります。ただし、読む人が迷わないよう、setTimeout には clearTimeout、setInterval には clearInterval と対にして書くのがおすすめです。
Q. setTimeoutの時間は正確ですか?
A. 指定した時間は「最短でこの時間」という意味で、ぴったりの保証ではありません。他の処理が動いていれば後ろにずれ、ブラウザでは入れ子が5回を超えると最小4ミリ秒、非アクティブタブではさらに間引かれます。経過時間を表示するときは、回数ではなく Date.now() の差で計算すると正確になります。
まとめ
- setTimeoutは1回、setIntervalは繰り返し。 止めるときは戻り値を
clearTimeout/clearIntervalに渡します。 - sleepは
new Promise((r) => setTimeout(r, ms))の1行。 Node.jsならtimers/promisesが標準で使えます。 - 待ちを途中でやめるにはAbortSignal。 中断時に
clearTimeoutで片付けるのがポイントです。 - setIntervalは処理が長いと重なる・詰まる。 入れ子setTimeoutにすると、終わってから次が始まります。
- ReactではuseEffectのクリーンアップで必ず止める。 StrictModeで2本動いたら書き忘れのサインです。
- メソッドを渡すときはアロー関数で包む。
thisが外れるエラーを避けられます。 - 遅延の上限は2^31-1ミリ秒(約24.8日)。 超えるとすぐ実行されるので、長い予約は日時を保存して判定します。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「JS setTimeout・setIntervalの使い方|sleepと停止」
株式会社RJCでは、setTimeoutやsetIntervalのようなJavaScriptの非同期処理の仕組みを仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。