setInterval вместе с setTimeout образует пару методов планирования вызовов в JavaScript. Формально это не часть языка, а Web API окна браузера (Window.setInterval); аналогичные таймеры есть и в Node.js. Метод решает задачу «выполнять функцию снова и снова с заданным шагом»: часы на странице, опрос API, ротация слайдов. У простых примеров есть обратная сторона, о которой молчит половина учебников: заявленный интервал не гарантирован, таймер дрейфует, а в фоновых вкладках браузеры замедляют его вплоть до одного срабатывания в минуту. Для контроля дрейфа пригодится Date.now().
Как это работает
Синтаксис: setInterval(func, delay, param1, ..., paramN). Первый аргумент – ссылка на функцию, второй – задержка в миллисекундах, дополнительные параметры передаются в колбэк при каждом вызове. Первое срабатывание происходит после первой задержки, а не сразу. Метод возвращает intervalID – положительное целое число, уникально идентифицирующее таймер. Его сохраняют, чтобы остановить повтор:
const id = setInterval(() => {
console.log("тик", new Date().toLocaleTimeString());
}, 1000);
// остановка
clearInterval(id);Четыре технических нюанса, которые всплывают в реальном коде:
- Общий пул ID – идентификаторы setInterval и setTimeout берутся из одного пула: технически clearInterval отменит и таймер setTimeout, но смешивать их не стоит.
- Максимальная задержка – delay хранится как 32-битное знаковое целое: максимум 2 147 483 647 мс (около 24,8 дня), при переполнении таймер срабатывает немедленно.
- Клампинг 4 мс – после пяти уровней вложенности таймеров задержка меньше 4 мс принудительно поднимается до 4 мс (требование HTML-стандарта).
- Строка вместо функции –
setInterval("doStuff()", 1000)работает как скрытый eval: риск XSS, а в окружениях с CSP и Trusted Types вызов блокируется. Передавайте только ссылку на функцию.
Почему таймер «плывёт» и чем это лечить
setInterval не ждёт завершения колбэка – запуски идут по расписанию. Время работы колбэка съедает часть интервала, поэтому реальная пауза между вызовами меньше заявленной. Если колбэк выполняется дольше delay, вызовы идут вплотную друг к другу. Добавьте event loop: функция запустится, только когда стек свободен, так что точная задержка не обещается в принципе – влияют нагрузка на процессор и другие задачи. Дрейф подтверждён и в Node.js (issue nodejs/node#21822).
Лекарство – рекурсивный setTimeout. Пауза отсчитывается от конца одного вызова до начала следующего, наложения исключены, а интервал можно менять на лету. MDN прямо рекомендует этот паттерн, когда логика может выполняться дольше интервала:
function poll() {
heavyOperation(); // занимает разное время
setTimeout(poll, 1000); // ровно 1 с после завершения
}
poll();| Способ | Пауза между вызовами | Наложения вызовов | Типовые задачи |
|---|---|---|---|
| setInterval | По расписанию; колбэк съедает часть интервала | Возможны при долгом колбэке | Часы, простой поллинг, слайдеры |
| Рекурсивный setTimeout | От конца вызова до начала следующего | Исключены | Опрос API, тяжёлые колбэки, динамический интервал |
| requestAnimationFrame | Синхронно с перерисовкой (обычно 60 кадров/с) | Кадр за кадром | Анимации; автопауза в фоновой вкладке |
Зачем это нужно
setInterval уместен там, где странице нужен регулярный «пульс»: обратный отсчёт до конца акции, обновление времени, периодическая проверка статуса заказа или платежа. Но у метода есть три ловушки, которые проявляются уже в продакшене.
Троттлинг фоновых вкладок. Firefox замедляет таймеры неактивных вкладок до минимум 1 секунды (кроме страниц с активным AudioContext). Chrome в фоне проверяет таймеры раз в секунду, а начиная с Chrome 88 (январь 2021) включает intensive-режим – раз в минуту, когда совпали четыре условия: вкладка в фоне дольше 5 минут, цепочка вложенных таймеров достигла 5, страница не издавала звук минимум 30 секунд, WebRTC не используется. Часы «раз в секунду» в фоновой вкладке физически не будут тикать чаще раза в минуту – это нужно учитывать в логике.
Утечки памяти. Пока интервал не отменён, движок держит ссылку на колбэк, а колбэк – своё замыкание с внешними переменными. Забытый setInterval в компоненте SPA или виджете – классическая утечка: очищайте интервал при размонтировании (cleanup-функция в useEffect, beforeUnmount и аналоги).
Потеря this. Метод, переданный напрямую (setInterval(counter.count, 1000)), теряет this – в строгом режиме это TypeError. Решения: стрелочная обёртка () => counter.count() или counter.count.bind(counter).
Когда setInterval не нужен вовсе. Анимации – зона requestAnimationFrame: он синхронизирован с перерисовкой и сам приостанавливается в фоне, тогда как интервал даёт рассинхрон с кадрами. Опрос ширины окна «каждые N миллисекунд» заменяется подпиской на события matchMedia, а опрос состояния DOM после действий пользователя – событийной моделью и делегированием событий. Правило: если существует событие, интервал не нужен.
Пример
Поллинг API с остановкой по условию – типовой продакшен-сценарий: страница оплаты раз в 5 секунд спрашивает бэкенд о статусе платежа и глушит таймер, получив ответ.
const id = setInterval(async () => {
const res = await fetch("/api/status");
const { done } = await res.json();
if (done) clearInterval(id);
}, 5000);Если важна точность на длинной дистанции (обратный отсчёт, секундомер), дрейф компенсируют сверкой с Date.now(): каждый шаг вычисляет накопившееся отставание и сокращает следующую паузу.
const start = Date.now();
let tick = 0;
function step() {
tick++;
const drift = Date.now() - start - tick * 1000;
setTimeout(step, Math.max(0, 1000 - drift));
}
setTimeout(step, 1000);Такой самокорректирующийся таймер держит ровный шаг даже при загруженном event loop – в отличие от «голого» setInterval, который со временем накапливает отставание.