Таймер в приложении — это механизм, который запускает код не сразу, а через заданное время (один раз) или повторяет его с заданным интервалом. В JavaScript для этого есть три инструмента: setTimeout (однократный запуск), setInterval (повтор) и requestAnimationFrame (запуск перед отрисовкой кадра в браузере). Задержка в них — минимальная, а не точная: функция выполнится не раньше указанного времени, но может позже.
Содержание
- Мини-словарь
- setTimeout и clearTimeout: запуск один раз и отмена
- Потеря this в колбэке таймера
- setInterval и clearInterval: повтор с интервалом
- Вложенный setTimeout против setInterval
- Нулевая задержка и порядок выполнения
- Пауза через await
- Пример: таймер обратного отсчета
- requestAnimationFrame: таймер для анимации
- Какой механизм выбрать
- Выводы
- Где применяется / связь с практикой
- FAQ
Примеры ниже проверены в Node.js 24 LTS; браузерные фрагменты (DOM, requestAnimationFrame) помечены отдельно.
Мини-словарь
| Термин | Что это | Пример |
|---|---|---|
| Задержка (delay) | Время в миллисекундах до запуска, 1000 мс = 1 с | setTimeout(fn, 1000) |
| Идентификатор таймера | То, что возвращает вызов; нужен для отмены | const id = setTimeout(...) |
| Интервал | Период между запусками повторяющегося таймера | setInterval(fn, 1000) |
| Цикл событий (event loop) | Механизм, который берет готовые задачи из очереди, когда основной код закончил работу | колбэк таймера ждет своей очереди |
Функции таймеров не входят в стандарт ECMAScript: их дает среда выполнения (браузер по спецификации HTML, Node.js — свой модуль timers). Поэтому поведение в деталях различается, ниже это оговорено.
setTimeout и clearTimeout: запуск один раз и отмена
Синтаксис: setTimeout(функция, задержка, ...аргументы). Аргументы после задержки передаются в функцию. Вызов возвращает идентификатор, по которому clearTimeout(id) отменяет запуск.
function greet(name, punctuation) {
console.log(`Привет, ${name}${punctuation}`);
}
const t1 = setTimeout(greet, 1000, "Анна", "!"); // через ~1 с
const t2 = setTimeout(greet, 500, "Борис", "."); // через ~0,5 с
clearTimeout(t2); // Борис не будет вызван
console.log("Таймеры запланированы");
Вывод:
Таймеры запланированы
Привет, Анна!
Строка «Таймеры запланированы» печатается первой: setTimeout не останавливает программу, а только ставит функцию в расписание. Второй таймер отменен до срабатывания, поэтому Борис не появляется.
Что важно знать про аргументы:
- Задержку можно не указывать — тогда она равна 0. В Node.js задержка меньше 1 мс (и
NaN) заменяется на 1 мс. - Задержка больше 2 147 483 647 мс (около 24,8 суток) не помещается в 32-битное число. В Node.js 24 такой таймер срабатывает почти сразу с предупреждением
TimeoutOverflowWarning: ... Timeout duration was set to 1. - В браузере первым аргументом формально можно передать строку кода, но это работает как
evalи блокируется политикой CSP. В Node.js 24 строка вызывает ошибкуERR_INVALID_ARG_TYPE. Передавайте функцию. - В браузере идентификатор — целое число, в Node.js — объект
Timeout. Храните его как есть и передавайте вclearTimeout, не рассчитывайте на тип.
Потеря this в колбэке таймера
Частая ошибка — передать метод объекта без привязки. Неверно:
const counter = {
label: "Счетчик",
count: 0,
tick() {
this.count += 1;
console.log(`${this.label}: ${this.count}`);
},
};
setTimeout(counter.tick, 100);
Результат в Node.js 24:
undefined: NaN
Таймер вызывает функцию отдельно от объекта, поэтому this — уже не counter (в Node.js это объект Timeout, в браузере — window). Исправление — обернуть вызов стрелочной функцией или привязать контекст через bind:
setTimeout(() => counter.tick(), 100); // вариант 1: стрелочная обертка
setTimeout(counter.tick.bind(counter), 200); // вариант 2: bind
Вывод:
Счетчик: 1
Счетчик: 2
В старом коде для этого встречается $.proxy() из jQuery — сегодня его заменяет нативный bind или стрелочная функция.
setInterval и clearInterval: повтор с интервалом
setInterval принимает те же аргументы, но вызывает функцию повторно, пока интервал не остановят через clearInterval.
let ticks = 0;
const id = setInterval(() => {
ticks += 1;
console.log(`Тик ${ticks}`);
if (ticks === 3) {
clearInterval(id);
console.log("Интервал остановлен");
}
}, 1000);
Вывод (по строке в секунду):
Тик 1
Тик 2
Тик 3
Интервал остановлен
Забытый clearInterval — типичная утечка: пока интервал активен, функция и все переменные, на которые она ссылается, остаются в памяти, а в Node.js активный таймер еще и не дает процессу завершиться.
Вложенный setTimeout против setInterval
Повтор можно сделать и цепочкой: функция в конце планирует саму себя. Разница видна, если работа внутри колбэка занимает время. В тесте ниже каждый вызов «работает» 60 мс при интервале 100 мс:
function busy(ms) { // имитация тяжелой работы, блокирует поток
const end = Date.now() + ms;
while (Date.now() < end) {}
}
const start = Date.now();
let n = 0;
const id = setInterval(() => {
n += 1;
console.log(`interval ${n}: ${Date.now() - start} мс`);
busy(60);
if (n === 4) clearInterval(id);
}, 100);
setTimeout(() => {
const s2 = Date.now();
let m = 0;
function step() {
m += 1;
console.log(`nested ${m}: ${Date.now() - s2} мс`);
busy(60);
if (m < 4) setTimeout(step, 100);
}
setTimeout(step, 100);
}, 600);
Вывод одного прогона (миллисекунды на другой машине будут немного другими):
interval 1: 101 мс
interval 2: 202 мс
interval 3: 302 мс
interval 4: 403 мс
nested 1: 101 мс
nested 2: 262 мс
nested 3: 422 мс
nested 4: 583 мс
setInterval |
вложенный setTimeout |
|
|---|---|---|
| Что выдерживает 100 мс | Период между стартами вызовов | Паузу между концом вызова и следующим стартом |
| Пауза после работы | 100 — 60 = 40 мс | 100 мс |
| Если работа дольше интервала | Вызовы идут подряд без паузы | Пауза сохраняется |
| Менять задержку на ходу | Нельзя, только пересоздать | Можно на каждом шаге |
Вывод: setInterval подходит для легкой периодической работы, вложенный setTimeout — когда шаг может затянуться или задержка зависит от результата.
Опрос сервера с увеличением паузы
Типичный сценарий вложенного таймера — повтор запроса с растущей паузой, если сервер перегружен. Здесь сервер имитирован, базовая пауза уменьшена до 100 мс для наглядности:
let calls = 0;
function fetchStatus() { // первые три вызова падают
calls += 1;
return calls <= 3 ? Promise.reject(new Error("503")) : Promise.resolve("ok");
}
const MAX = 800; // потолок паузы, мс
let delay = 100; // в реальном сервисе, например, 5000
let attempts = 0;
const start = Date.now();
function poll() {
attempts += 1;
fetchStatus()
.then((status) => console.log(`${Date.now() - start} мс: ответ ${status}`))
.catch((err) => {
if (attempts >= 6) return console.log("Лимит попыток исчерпан");
console.log(`${Date.now() - start} мс: ошибка ${err.message}, пауза ${delay} мс`);
setTimeout(poll, delay);
delay = Math.min(delay * 2, MAX);
});
}
poll();
Вывод одного прогона (миллисекунды меняются от запуска к запуску):
1 мс: ошибка 503, пауза 100 мс
112 мс: ошибка 503, пауза 200 мс
313 мс: ошибка 503, пауза 400 мс
713 мс: ответ ok
Потолок паузы и лимит попыток обязательны: без них при долгом сбое клиент будет ждать бесконечно.
Нулевая задержка и порядок выполнения
setTimeout(fn, 0) не запускает функцию немедленно. Колбэк попадает в очередь задач и выполнится после текущего синхронного кода и после микрозадач (обработчиков Promise):
console.log("1: синхронный код");
setTimeout(() => console.log("4: setTimeout 0"), 0);
Promise.resolve().then(() => console.log("3: микрозадача Promise"));
console.log("2: синхронный код");
Вывод:
1: синхронный код
2: синхронный код
3: микрозадача Promise
4: setTimeout 0
Задержка в браузере еще и ограничена снизу: по спецификации HTML после пяти уровней вложенных таймеров она не меньше 4 мс, а во вкладке в фоне браузеры обычно запускают таймеры не чаще раза в секунду. Chrome для вкладки, скрытой дольше 5 минут и без звука, ограничивает цепочки из 5 и более вложенных таймеров еще сильнее — до раза в минуту.
Пауза через await
Для «подождать N миллисекунд» внутри асинхронной функции таймер оборачивают в Promise. В Node.js есть готовый вариант в node:timers/promises (файл .mjs, ESM):
import { setTimeout as sleep } from "node:timers/promises";
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
console.log("Старт");
await wait(500);
console.log("Прошло ~0,5 с (Promise-обертка)");
await sleep(500);
console.log("Прошло еще ~0,5 с (node:timers/promises)");
Обертка wait работает и в браузере, импорт node:timers/promises — только в Node.js.
Пример: таймер обратного отсчета
Наивный отсчет уменьшает счетчик на 1 каждую секунду. Он «уплывает»: каждый вызов приходит чуть позже, а в фоновой вкладке таймеры замедляются. Надежнее хранить момент окончания (deadline) и на каждом тике считать остаток от текущего времени:
function formatRemaining(ms) {
const total = Math.max(0, Math.ceil(ms / 1000));
const m = String(Math.floor(total / 60)).padStart(2, "0");
const s = String(total % 60).padStart(2, "0");
return `${m}:${s}`;
}
function startCountdown(seconds, onTick, onDone) {
if (!Number.isFinite(seconds) || seconds < 0) {
throw new RangeError(`seconds: нужно число >= 0, получено ${seconds}`);
}
const deadline = Date.now() + seconds * 1000;
let timerId = null;
let stopped = false;
function tick() {
const left = deadline - Date.now();
onTick(formatRemaining(left));
if (stopped) return; // stop() вызвали внутри onTick
if (left <= 0) {
onDone();
return;
}
// следующий тик - к ближайшей границе секунды, а не ровно через 1000 мс
timerId = setTimeout(tick, left % 1000 || 1000);
}
tick();
return () => { stopped = true; clearTimeout(timerId); }; // функция остановки
}
console.log(formatRemaining(0), formatRemaining(1), formatRemaining(61000), formatRemaining(599001));
const stop = startCountdown(3, (text) => console.log(text), () => console.log("Время вышло"));
Вывод (строки отсчета — по одной в секунду):
00:00 00:01 01:01 10:00
00:03
00:02
00:01
00:00
Время вышло
Первая строка проверяет форматирование на границах: 0 мс, 1 мс (округляется вверх до секунды), 61 с и 599,001 с. startCountdown возвращает функцию stop — вызовите ее, чтобы прервать отсчет.
Две защиты в коде не для красоты. Без проверки входа startCountdown(NaN, ...) печатает NaN:NaN раз в секунду бесконечно: остаток NaN никогда не проходит условие left <= 0. Теперь NaN, строка "10" и отрицательное число сразу дают RangeError. Флаг stopped нужен, чтобы stop() работала и внутри onTick: без него clearTimeout отменял бы уже сработавший таймер, а следующий тик все равно планировался бы строкой ниже.
Чтобы вывести отсчет на страницу, те же функции подключаются к разметке (браузерный фрагмент):
<p id="clock">00:10</p>
<button id="go">Старт</button>
<script>
// сюда вставить formatRemaining и startCountdown из примера выше
const clock = document.getElementById("clock");
let stop = null;
document.getElementById("go").addEventListener("click", () => {
if (stop) stop(); // повторный клик перезапускает отсчет
stop = startCountdown(10,
(text) => { clock.textContent = text; },
() => { clock.textContent = "Время вышло"; });
});
</script>
textContent вместо innerHTML — чтобы в элемент не попала разметка.
requestAnimationFrame: таймер для анимации
requestAnimationFrame(callback) вызывает функцию один раз перед следующей отрисовкой кадра, передавая ей метку времени. Частота совпадает с частотой обновления экрана (часто 60 Гц, но бывает 120 Гц и выше), а в фоновой вкладке вызовы приостанавливаются. Поэтому скорость анимации считают от прошедшего времени, а не от числа кадров (браузерный фрагмент):
const box = document.getElementById("box");
let startTime = null;
let frameId = null;
function rotate(now) {
if (startTime === null) startTime = now;
const angle = ((now - startTime) / 1000) * 90; // 90 градусов в секунду
box.style.transform = `rotate(${angle}deg)`;
frameId = requestAnimationFrame(rotate);
}
frameId = requestAnimationFrame(rotate);
// остановка: cancelAnimationFrame(frameId);
Какой механизм выбрать
| Задача | Что взять |
|---|---|
| Выполнить действие один раз через время | setTimeout |
| Легкая периодическая работа (часы, опрос раз в минуту) | setInterval |
| Повтор с долгим шагом или переменной паузой | вложенный setTimeout |
| Анимация и изменение отрисовки | requestAnimationFrame |
| Пауза в async-функции | await + Promise-обертка над setTimeout |
| Точный отсчет до события | deadline + Date.now() на каждом тике |
Выводы
- Таймер в приложении запускает код через заданное время или с интервалом; в JavaScript это
setTimeout,setIntervalиrequestAnimationFrame, их предоставляет среда (браузер, Node.js). - Задержка — минимальная, а не гарантированная: колбэк ждет окончания текущего кода и микрозадач.
- Каждый таймер нужно уметь остановить: сохраняйте идентификатор и вызывайте
clearTimeout/clearInterval/cancelAnimationFrame. - Вложенный
setTimeoutдержит паузу между вызовами и позволяет менять задержку,setIntervalдержит период между стартами. - Обратный отсчет считайте от момента окончания, а не уменьшением счетчика, иначе он будет отставать.
Где применяется / связь с практикой
Освойте тему на практике
Таймеры встречаются почти в каждом интерфейсе: автосохранение черновика, задержка перед поиском при вводе, повтор неудачного запроса, слайдеры, отсчет до конца акции или теста. Чтобы уверенно писать такой код, нужно понимать асинхронность и цикл событий целиком — это разбирают на курсе «JavaScript-разработчик. Базовый уровень». Попробовать формат обучения можно на бесплатных открытых уроках Otus.
FAQ
Можно ли остановить таймер изнутри его же колбэка?
Да. Для setInterval так и делают — вызывают clearInterval(id) внутри функции, как в примере выше. Для setTimeout отмена изнутри не нужна: он уже сработал; достаточно не планировать следующий вызов.
Почему таймер в фоновой вкладке «тормозит»?
Браузеры экономят ресурсы: в скрытой вкладке таймеры запускаются реже, а requestAnimationFrame приостанавливается. Поэтому время и остаток отсчета берут из Date.now() или performance.now(), а не из числа сработавших тиков.
Чем отличается clearTimeout от clearInterval?
В браузере оба работают с общим списком идентификаторов, и технически взаимозаменяемы, но для читаемости вызывайте парную функцию: clearTimeout для setTimeout, clearInterval для setInterval.



