Точный таймер каждый раз считает разницу между Date.now() и дедлайном. Он не вычитает секунду вслепую, поэтому смена вкладки или задержка браузера не накопит большую ошибку.
setInterval обещает вызвать функцию примерно через заданный интервал, но не гарантирует точное расписание. Вкладка может уснуть, основной поток может быть занят. Поэтому источник истины один: абсолютное время конца.
Храним timestamp дедлайна, а не число оставшихся тиков.
Переводим миллисекунды в дни, часы, минуты и секунды.
На нуле очищаем интервал и показываем завершение.
Как хранить дедлайн и остаток
В HTML достаточно четырёх значений и подписи. JavaScript получает deadline один раз, а функция render на каждом вызове заново вычисляет remaining. Math.max не даёт уйти в отрицательные числа.
| Значение | Делитель | После деления |
|---|---|---|
| Дни | 86 400 000 | floor |
| Часы | 3 600 000 | остаток по 24 |
| Минуты | 60 000 | остаток по 60 |
| Секунды | 1 000 | остаток по 60 |
После этого шага у каждой части есть одна роль. Интерфейс собирает действие, логика меняет состояние, а вывод показывает признак успеха. Это проще отлаживать, чем один большой обработчик.

Время каждый раз считается заново. На нуле interval очищается
Пишем расчёт и обновление интерфейса
В примере дедлайн задан на 26 часов вперёд, чтобы были видны и дни, и часы. В реальном проекте передавайте ISO-дату с часовым поясом. format с padStart сохраняет две цифры, а interval очищается ровно один раз.
const deadline = Date.now() + 26 * 60 * 60 * 1000;
const output = document.querySelector('[data-countdown]');
const pad = value => String(value).padStart(2, '0');
function getParts(milliseconds) {
const total = Math.max(0, Math.floor(milliseconds / 1000));
return {
days: Math.floor(total / 86400),
hours: Math.floor(total / 3600) % 24,
minutes: Math.floor(total / 60) % 60,
seconds: total % 60,
};
}
function render() {
const remaining = deadline - Date.now();
const time = getParts(remaining);
output.textContent = time.days + ' дн. ' + pad(time.hours) + ':' + pad(time.minutes) + ':' + pad(time.seconds);
if (remaining <= 0) {
clearInterval(interval);
output.textContent = 'Время вышло';
}
}
render();
const interval = setInterval(render, 250);Дедлайн хранится абсолютным timestamp. Интерфейс обновляется сразу, без первой пустой секунды. Пауза фоновой вкладки не меняет итоговый момент. На нуле интервал очищается, отрицательных чисел нет.

Пауза вкладки показывает разницу. Сравнение показывает, какой вариант легче поддерживать и проверять.
Почему setInterval не является секундомером
Интервал 250 миллисекунд не делает таймер точнее как часы. Он лишь быстрее замечает переход на новую отображаемую секунду. Точность даёт повторный расчёт deadline - Date.now. Если вкладка была заморожена минуту, следующий render сразу покажет актуальный остаток.
| Подход | После паузы вкладки | Ошибка |
|---|---|---|
| seconds-- | Продолжает со старого числа | Накапливается |
| deadline - Date.now() | Прыгает к актуальному времени | Не накапливается |
| requestAnimationFrame | Обычно заморожен в фоне | Не нужен для секунд |
| Web Worker | Сложнее архитектура | Избыточен для виджета |
Сохраняйте в одном месте то, что может измениться. Если одно и то же значение скопировано в три функции, после первой правки они начнут расходиться. Один объект состояния и одна функция отрисовки снимают эту проблему.
Практика: проверяем паузу и ноль
Поставьте дедлайн на десять секунд, переключитесь на другую вкладку на пять секунд и вернитесь. Затем задайте время в прошлом и обновите страницу. В обоих случаях интерфейс должен прийти к правильному нулю без отрицательных значений. Повторите опыт после перевода системных часов и запишите, какое ограничение клиентского времени это показывает.
- Поставьте дедлайн на 10 секунд.
- Откройте другую вкладку на половину времени.
- Вернитесь и сравните остаток с часами.
- Задайте дедлайн в прошлом.
- Проверьте формат 01:05:09.
- Добавьте aria-live="polite" только итоговой подписи, не всему секундному счётчику.
После фоновой паузы время остаётся верным. Часы и минуты всегда двузначные. Дедлайн в прошлом сразу показывает завершение. Интервал очищается на нуле.

Таймер готов после четырёх проверок. Пройдите все четыре сценария перед публикацией проекта.
Доводим таймер до готового виджета
Для события из сервера передавайте дату вместе с часовым поясом, например ISO-строку. Не собирайте дату вручную из локальных фрагментов. Если момент критичен, сервер должен оставаться источником истины, а клиентский таймер только отображает состояние.
Задержки браузера накапливают ошибку.
Экран пустует до первого интервала.
Ограничьте Math.max и остановите interval.
У пользователей получится разный момент.
Что получится в итоге

Готовый виджет показывает день, часы, минуты и секунды, переживает паузу вкладки и заканчивает ровно на нуле.
Почему нельзя просто уменьшать число?
Вызовы интервала могут задерживаться, и ошибка будет накапливаться.
Что является источником истины?
Абсолютный timestamp дедлайна.
Зачем обновлять чаще секунды?
Чтобы быстрее заметить границу отображаемой секунды, но не для измерения времени.
Как понять, что проект готов?
Он повторяет целевой сценарий несколько раз подряд, понятно реагирует на ошибочный ввод и не теряет состояние при обычных действиях.
Можно ли копировать код целиком?
Можно как стартовую точку, но сначала замените значения из примера, затем запустите проект по шагам. Если вы не можете объяснить одну строку, её лучше разобрать до следующего шага.
Даты, функции и DOM можно отработать в курсе JavaScript. Подключение файла проверьте по короткой инструкции.
Если нужен интерфейс события, соедините таймер с модальным окном или сохраните настройки по примеру Todo-листа. В Кодике удобно тестировать короткие дедлайны и крайние значения.