TGViewer
Дивовижний світ веброзробки Дивовижний світ веброзробки @babichdev · 2.91K subscribers
Post #447 1.4K
Днями на редіті побачив допис, в якому автор питав, чому його лічильник часу на сторінці за пів години активності відстає аж на 30 секунд.

Задача, здавалось би, одна з найпростіших — запускай таймер, рахуй секунди, виводь і буде тобі щастя. Але річ в тому, що багато хто досі вважає таймери в JS точними.

На ділі що setTimeout, що setInterval не виконують ваш код через заданий проміжок часу. Вони ставлять його в чергу. Це суттєва різниця. Тобто це не "виконай через точно 1000ms", а радше "я тут вам задачку залишив, запусти за нагоди, але не раніше ніж 1000ms". Це якщо прям дуже приблизно. Я не буду вдаватися в деталі, там вам і улюблений event loop, і черги, і так звана однопоточність.

Давайте ліпше розберемо, як уникнути такого дрифта або хоча б зменшити відхилення до меж статистичної похибки.

Перше, що прийде нам в голову для такого лічильника, — запустити setInterval:
setInterval(() => doMagic(), 1000);


Це найнаївніший спосіб. setInterval намагається запускати callback із заданою періодичністю, але не гарантує момент його фактичного виконання. Якщо main thread зайнятий, виклик відбудеться пізніше. Якщо ж ми просто робимо seconds++, то починаємо вимірювати не час, а кількість викликів — і рано чи пізно ці дві величини можуть розійтися.

Кращим підходом буде запускати лічильник, коли попередня ітерація точно відпрацювала. Але просто переробляти на рекурсивний setTimeout не треба, бо він так само накопичуватиме затримку:
const tick = () => {
…
setTimeout(() => tick(), 1000);
}
tick();


Краще розраховувати наступний таймаут як залишок до бажаного моменту в майбутньому:
let next = performance.now() + 1000;
const tick = () => {
next += 1000;

const delay = Math.max(0, next - performance.now());
setTimeout(tick, delay);
}
setTimeout(tick, 1000);


Оцей delay буде трошки менше за 1000ms, бо враховує похибку попередньої ітерації.

Це працює в ідеальних умовах, коли основний потік нічим суттєво не зайнятий. Але setTimeout не гарантує, що callback виконається саме в заданий момент. Якщо main thread зайнятий JavaScript, layout, rendering чи іншою роботою, callback чекатиме. Якщо потік буде заблокований більше секунди, ми можемо взагалі проскочити момент оновлення інтерфейсу.

А оскільки нам зрештою треба не просто щось порахувати, а намалювати нове значення, можна передати останній крок requestAnimationFrame. Він не робить таймер точнішим і не обходить заблокований main thread — він просить браузер виконати оновлення перед наступним repaint.
let next = performance.now() + 1000;

const tick = () => {
console.log('tick');

next += 1000;

const delay = Math.max(0, next - performance.now());
setTimeout(() => requestAnimationFrame(tick), delay);
};

setTimeout(() => requestAnimationFrame(tick), 1000);


Тут requestAnimationFrame розвʼязує не проблему точності, а іншу: якщо результат треба показати користувачу, ми можемо синхронізувати DOM-update з наступним кадром браузера. Аби не перескакувати секунди, варто ще знати, скільки часу пройшло з останнього виконання:
let previous = performance.now();
let seconds = 0;

const tick = (now) => {
// Ось тут визначаємо, скільки реально минуло секунд
// і на скільки нам треба "стрибнути" вперед
const elapsed = now - previous;
previous = now;
seconds += elapsed / 1000;
…
setTimeout(() => requestAnimationFrame(tick), 1000);
};

requestAnimationFrame(tick);


Такий підхід дозволяє максимально точно рахувати "тіки". Але є одне але, якого ну ніяк не уникнути — якщо ваш UI таки важкий, то іноді лічильник буде прострибувати значення не через рівні проміжки часу, наприклад 0.5с > 1с > 3с. Але при цьому показуватиме точний час. Це вимушений трейдоф через завантажений основний потік. Але це вже проблема не лічильника, а ваша — бо чого це у вас така важка сторінка, га?

Що почитати:
MDN — setTimeout()
MDN — requestAnimationFrame()

Що почитати душнілам:
HTML Living Standard — Timers

---

Збір на авто для 21 ОМБр триває, зібрано 22 200грн з необхідних 271 500грн.

🔗 send.monobank.ua/jar/AeXQ6YRf2X
💳 5375411202918178
  • ❤ 24
  • 🔥 9
  • 👍 6
More from @babichdev
  1. Sep 30, 2026Товариство, запрошую вас цієї суботи, 3 жовтня, на Fwdays Tech Summit — онлайн конференцію…
  2. Sep 28, 2026#збір_на_авто_для_21 Товариство, почнімо тиждень з доброго діла. Я би дуже хотів, аби ми ц…
  3. Sep 27, 2026Знайшов своє старезне резюме. Аж пустив скупу сльозу за тими часами, коли навіть з таким м…
  4. Sep 23, 2026Який ШІ найкращий для навчання? Відповідь проста — той, з яким ви чогось навчились. ШІ це…
  5. Sep 18, 2026Оце я, канєшна, провтикав. Конфа завтра, 19 вересня. Ще встигаєте взяти квиточок.
  6. Sep 15, 2026#партнерський_допис Товариство, запрошую вас цієї пʼятниці, 18 вересня, на найкулуарнішу к…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →