Skip to main content

Гарантія виконання таймера

Ні, гарантувати точний час виконання таймера в JavaScript (setTimeout, setInterval тощо) неможливо. Причина проста: JavaScript однопотоковий, і момент запуску таймера залежить від стану Event Loop і завантаженості стека викликів.

Теорія

TL;DR

  • Таймер гарантує лише те, що колбек не виконається раніше за вказану затримку.
  • Поки стек викликів зайнятий, колбек чекає у черзі макрозадач.
  • Навіть setTimeout(fn, 0) має мінімальну затримку, у браузерах це близько 4 мс.
  • У неактивній вкладці браузер навмисно сповільнює таймери, до 1000 мс і більше.
  • setInterval теж не дає рівних інтервалів: повільний колбек зсуває всі наступні тики.
  • Для точності є performance.now(), requestAnimationFrame() і схема з компенсацією дрейфу.

Швидкий приклад

javascript
console.log('Старт'); setTimeout(() => console.log('Таймер 1s'), 1000); const start = Date.now(); while (Date.now() - start < 3000) {} // блокуємо потік на 3 секунди console.log('Кінець');

Результат:

text
Старт (3 секунди паузи) Кінець Таймер 1s

Хоча таймер був на 1 секунду, він спрацював лише через 3 секунди. Чому? Тому що рушій не міг виконати його раніше, стек був зайнятий циклом.

Чому так відбувається

Коли ви викликаєте:

javascript
setTimeout(fn, 1000);

JavaScript не обіцяє, що fn виконається рівно через 1000 мс. Він лише ставить задачу в чергу, щоб виконати її не раніше, ніж через 1000 мс.

Якщо в момент, коли таймер «спрацював», стек викликів зайнятий, колбек чекатиме своєї черги.

Мінімальна затримка і throttling

Навіть setTimeout(fn, 0) не запускає код «одразу», мінімальна затримка все одно є:

  • у браузерах зазвичай 4 мс для вкладених таймерів, за стандартом HTML Living Standard;
  • у Node.js вона може бути меншою, але теж не миттєва.

Крім того, якщо вкладка браузера неактивна, затримку може бути збільшено до 1000 мс і більше: браузери економлять ресурси.

Приклад із setInterval

setInterval() теж не гарантує точних інтервалів. Якщо функція всередині виконується довше, ніж сам інтервал, виклики «накопичуються» і виконуються із запізненням.

javascript
setInterval(() => { const start = Date.now(); while (Date.now() - start < 1500) {} // блокуємо на 1.5 секунди console.log('tick'); }, 1000);

Інтервал 1 секунда, але функція займає 1.5 секунди, тому «тіки» йдуть із реальною затримкою 1.5, 3.0, 4.5 секунди і так далі.

Чому точність недосяжна

ПричинаПояснення
Однопотоковість JavaScriptПоки виконується код, інші задачі чекають
Event LoopТаймери лише потрапляють у чергу, вони не запускаються миттєво
Обмеження браузераЄ мінімальна затримка, а у фоні ще й сповільнення
Навантаження на CPUЗа високого навантаження таймери виконуються пізніше
Планувальник ОСJavaScript не керує системним часом, він лише «просить» запустити задачу пізніше

Що можна зробити

Якщо потрібна максимальна точність, наприклад в анімації або синхронізації, є альтернативи.

  1. performance.now() вимірює точний час у мілісекундах, із часткою до мікросекунд:

    javascript
    const start = performance.now(); // ... const elapsed = performance.now() - start;
  2. requestAnimationFrame() запускає колбек перед кожним кадром, приблизно 60 разів на секунду, і ідеально підходить для плавних анімацій.

  3. setTimeout з компенсацією дрейфу, тобто з корекцією затримки:

    javascript
    const start = Date.now(); let count = 0; function tick() { count++; const expected = start + count * 1000; const drift = Date.now() - expected; console.log(`tick ${count}, drift: ${drift}ms`); setTimeout(tick, 1000 - drift); } tick();

    Такий підхід враховує «запізнення» і вирівнює інтервали.

Підсумок

ПитанняВідповідь
Чи можна гарантувати точний час таймера?Ні
Чому?Однопотоковість JavaScript, Event Loop, мінімальні затримки і навантаження
Що відбувається насправді?Таймер додається у чергу задач і виконується пізніше
Чи є альтернатива для точності?Так: requestAnimationFrame, performance.now, компенсація дрейфу

Типові помилки

  • Вважати затримку розкладом. delay це нижня межа, а не момент запуску.
  • Будувати годинник на setInterval. За годину такий лічильник помітно відстане; беріть за основу реальний час через Date.now() або performance.now() і перемальовуйте значення.
  • Вимірювати тривалість через Date.now(). Системний час може змінитися, зокрема під час синхронізації; performance.now() монотонний і для вимірювань надійніший.
  • Забувати про фонові вкладки. Логіка, що спирається на рівний тик таймера, ламається, щойно користувач перемкнув вкладку.
  • Анімувати через setInterval. Кадрову анімацію робить requestAnimationFrame, він синхронізований з оновленням екрана і не дає розривів.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.