Гарантія виконання таймера
Ні, гарантувати точний час виконання таймера в JavaScript (setTimeout, setInterval тощо) неможливо. Причина проста: JavaScript однопотоковий, і момент запуску таймера залежить від стану Event Loop і завантаженості стека викликів.
Теорія
TL;DR
- Таймер гарантує лише те, що колбек не виконається раніше за вказану затримку.
- Поки стек викликів зайнятий, колбек чекає у черзі макрозадач.
- Навіть
setTimeout(fn, 0)має мінімальну затримку, у браузерах це близько 4 мс. - У неактивній вкладці браузер навмисно сповільнює таймери, до 1000 мс і більше.
setIntervalтеж не дає рівних інтервалів: повільний колбек зсуває всі наступні тики.- Для точності є
performance.now(),requestAnimationFrame()і схема з компенсацією дрейфу.
Швидкий приклад
console.log('Старт');
setTimeout(() => console.log('Таймер 1s'), 1000);
const start = Date.now();
while (Date.now() - start < 3000) {} // блокуємо потік на 3 секунди
console.log('Кінець');Результат:
Старт
(3 секунди паузи)
Кінець
Таймер 1sХоча таймер був на 1 секунду, він спрацював лише через 3 секунди. Чому? Тому що рушій не міг виконати його раніше, стек був зайнятий циклом.
Чому так відбувається
Коли ви викликаєте:
setTimeout(fn, 1000);JavaScript не обіцяє, що fn виконається рівно через 1000 мс. Він лише ставить задачу в чергу, щоб виконати її не раніше, ніж через 1000 мс.
Якщо в момент, коли таймер «спрацював», стек викликів зайнятий, колбек чекатиме своєї черги.
Мінімальна затримка і throttling
Навіть setTimeout(fn, 0) не запускає код «одразу», мінімальна затримка все одно є:
- у браузерах зазвичай 4 мс для вкладених таймерів, за стандартом HTML Living Standard;
- у Node.js вона може бути меншою, але теж не миттєва.
Крім того, якщо вкладка браузера неактивна, затримку може бути збільшено до 1000 мс і більше: браузери економлять ресурси.
Приклад із setInterval
setInterval() теж не гарантує точних інтервалів. Якщо функція всередині виконується довше, ніж сам інтервал, виклики «накопичуються» і виконуються із запізненням.
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 не керує системним часом, він лише «просить» запустити задачу пізніше |
Що можна зробити
Якщо потрібна максимальна точність, наприклад в анімації або синхронізації, є альтернативи.
-
performance.now()вимірює точний час у мілісекундах, із часткою до мікросекунд:javascriptconst start = performance.now(); // ... const elapsed = performance.now() - start; -
requestAnimationFrame()запускає колбек перед кожним кадром, приблизно 60 разів на секунду, і ідеально підходить для плавних анімацій. -
setTimeoutз компенсацією дрейфу, тобто з корекцією затримки:javascriptconst 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, він синхронізований з оновленням екрана і не дає розривів.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.