Почему онлайн-таймер опаздывает в фоновой вкладке

Онлайн-таймер может опоздать, потому что браузер не обещает запускать JavaScript ровно каждую секунду. В скрытой вкладке обновления ограничиваются, при высокой нагрузке обработчик ждёт свободного главного потока, а во время сна устройства код страницы вообще не выполняется. Поэтому точный отсчёт хранят как момент окончания, а экран и звук считают отдельными задачами.

Почему онлайн-таймер опаздывает в фоновой вкладке

Ниже проведём опыт с таймером на 90 секунд, сравним счётчик тиков с абсолютным временем и разберём, что произойдёт после переключения вкладки, заморозки или сна ноутбука. В конце будет практическая памятка: когда браузерного таймера достаточно, а когда нужен системный будильник.

В статье вы узнаете:

У таймера есть три независимые части

Когда на экране написано «01:30», кажется, что работает один механизм. На самом деле браузер решает три задачи.

Отсчёт хранит ответ на вопрос, сколько времени осталось. Его можно построить по конечному моменту: время завершения минус текущее время.

Экран время от времени показывает этот ответ. Цифры меняются только тогда, когда браузер дал странице выполнить код и перерисовать интерфейс.

Сигнал пытается привлечь внимание после окончания. Звук может быть заблокирован политикой браузера, вкладка может быть заморожена, а устройство может спать.

Поэтому фраза «таймер шёл в фоне» неоднозначна. После возвращения цифры могут сразу показать ноль, потому что момент окончания давно прошёл. Но экран не обновлялся каждую секунду, а звук мог прозвучать только после пробуждения.

Задержка означает «не раньше», а не «ровно тогда»

В JavaScript можно попросить браузер вызвать функцию через секунду:

setTimeout(update, 1000);

Эта запись не бронирует процессор на точный момент. Она означает, что обработчик станет готов к выполнению после минимальной задержки. Затем он попадёт в очередь задач и дождётся, пока завершится текущая работа.

Даже в активной вкладке опоздание может создать тяжёлое вычисление, обработка большого события или сложная перерисовка. MDN перечисляет причины задержек setTimeout и отдельно описывает ограничения для неактивных вкладок.

Простой опыт: запланируйте обработчик через 100 миллисекунд, а затем на три секунды займите главный поток непрерывным циклом. Обработчик не прервёт уже выполняющийся код. Он запустится примерно через три секунды, хотя просили сто миллисекунд.

Часы при этом не остановились. Страница просто не получила возможность выполнить следующую задачу.

Почему счётчик тиков начинает отставать

Наивный таймер часто устроен так:

let remaining = 90;

setInterval(() => {
  remaining -= 1;
  draw(remaining);
}, 1000);

Здесь каждая выполненная функция считается прошедшей секундой. Если браузер за реальную минуту запустил только десять обработчиков, переменная уменьшится на десять. На экране останется 80 секунд, хотя должно остаться 30.

Ошибка накапливается даже без фоновой вкладки. Каждый вызов может опаздывать на несколько миллисекунд, а код всё равно вычитает ровно одну условную секунду.

Такой подход путает часы с уведомлением «пора обновить экран». Частота обработчиков не измеряет прошедшее физическое время.

Конечный момент не зависит от числа обновлений

Для таймера на 90 секунд, запущенного в 12:00:00, можно сразу сохранить конечный момент 12:01:30. При любом обновлении остаётся выполнить одно вычитание:

остаток = 12:01:30 − текущее время.

Код выглядит так:

const deadline =
  Date.now() + 90 * 1000;

function sync() {
  const msLeft = Math.max(
    0,
    deadline - Date.now(),
  );
  const secondsLeft =
    Math.ceil(msLeft / 1000);
  draw(secondsLeft);
}

setInterval(sync, 1000);

Интервал всё ещё может опоздать. Разница в том, что опоздание больше не меняет правду. Если функция не выполнялась 40 секунд, при первом доступном вызове она увидит текущее время и сразу покажет правильный остаток.

Именно так теперь работает онлайн-таймер eCalc: хранит конечный момент, сверяется с часами при каждом обновлении и дополнительно синхронизируется после возврата на страницу. Пауза сохраняет вычисленный остаток, а продолжение создаёт новый конечный момент.

Счётчик по выполненным тикам отстаёт после пропуска обновлений, а отсчёт по конечному моменту сразу догоняет часы
Верхняя шкала считает выполненные callbacks и теряет пропуски. Нижняя хранит конечный момент и при следующей сверке сразу показывает правильный остаток.

Контрольный опыт на 90 секунд

Проведите опыт не с важной встречей, а с коротким интервалом.

  1. Откройте таймер eCalc на 1 минуту 30 секунд.
  2. Запишите время старта по системным часам, например 12:00:00.
  3. Нажмите «Старт» и сразу перейдите в другую вкладку.
  4. Вернитесь около 12:01:00.
  5. Сравните показание с системными часами.

У таймера по конечному моменту останется примерно 30 секунд. Экран мог не отрисовать промежуточные значения, но после возврата догонит часы одним расчётом.

Затем повторите опыт и оставьте устройство спать до 12:02:00. Если вкладка сохранилась в памяти, после пробуждения таймер покажет ноль и попытается воспроизвести разрешённый звук. Если браузер выгрузил или перезагрузил вкладку, текущая версия eCalc не восстановит отсчёт. В любом случае страница не могла подать сигнал в 12:01:30, пока система не исполняла её код.

Этот опыт отдельно проверяет состояние цифр и своевременность сигнала. Один успешный звук не доказывает, что браузер всегда разбудит страницу.

Если цифры прыгнули с 00:30 на 00:00, это не означает, что отсчёт потерял полминуты. Экран долго не обновлялся, а затем сразу сверился с конечным моментом. Если после минуты в фоне осталось почти столько же, сколько до переключения, программа, вероятно, вычитает по секунде за каждый выполненный callback и действительно накапливает пропущенные тики. Если ноль показан правильно, но звук прозвучал поздно, арифметика и канал оповещения повели себя по-разному.

Экран, отсчёт по часам и звуковой сигнал являются тремя отдельными частями браузерного таймера
Экран можно синхронизировать после возврата, отсчёт хранить как deadline, а своевременный звук всё равно зависит от работы вкладки и разрешений браузера.

Что происходит, когда вкладка становится скрытой

Браузеры экономят процессор и батарею. Для невидимой страницы нет смысла рисовать 60 кадров в секунду или строго каждую секунду менять цифру, которую пользователь не видит.

Поэтому фоновые таймеры могут объединяться в редкие проверки. После долгого нахождения в фоне ограничение бывает ещё сильнее. Точные пороги отличаются между браузерами и версиями, поэтому нельзя строить продукт на конкретном обещании «раз в секунду» или «раз в минуту».

Page Visibility API сообщает странице, что она стала скрытой или снова видимой. Это полезный сигнал: при возвращении можно немедленно сверить экран с конечным моментом. Но событие не запрещает браузеру позже заморозить или выгрузить вкладку.

Потеря фокуса и скрытие тоже не одно и то же. Окно может оставаться видимым рядом с другим, хотя клавиатурный фокус ушёл. Для обновления таймера полезно реагировать и на видимость, и на возвращение фокуса, не считая ни одно событие гарантией фоновой работы.

Ограничение, заморозка и выгрузка различаются

Ограничение частоты означает, что код выполняется, но реже. Таймер по конечному моменту восстановит правильные цифры при следующем вызове.

Заморозка приостанавливает задачи страницы. Состояние остаётся в памяти, но обработчики не выполняются. После разморозки конечный момент всё ещё известен и остаток можно пересчитать.

Выгрузка вкладки удаляет её из памяти. После открытия страница запускается заново. Чтобы восстановить незавершённый таймер, нужно заранее сохранить конечный момент в хранилище браузера. Само хранилище не подаст сигнал: оно только позволит восстановить состояние после перезагрузки.

Сон устройства останавливает выполнение страницы вместе с остальными обычными программами. После пробуждения настенные часы показывают, сколько времени прошло, но пропущенный звук невозможно отправить в прошлое.

Для бытового таймера нужны настенные часы

Date.now() использует системное настенное время. Оно подходит для таймера «через десять минут» и обратного отсчёта до календарного события: после сна можно сравнить сохранённый момент с текущей датой. Минус в том, что ручной перевод часов или системная коррекция способны сдвинуть остаток.

Монотонный счётчик performance.now() полезен для секундомера и измерения короткой операции: перевод системного времени его не сдвигает. Но поведение во время сна различается между платформами. Для бытового таймера, который должен учитывать сон, абсолютный момент по настенным часам практичнее.

Обратный отсчёт eCalc хранит целевую календарную дату и при каждом обновлении заново вычитает текущую. Поэтому пропущенные тики не накапливают ошибку. Как и у обычного таймера, экран способен догнать дату после возврата, а звук всё равно зависит от того, получил ли браузер возможность выполнить код.

Почему звук является отдельной проблемой

Даже правильный deadline не создаёт системный будильник. Страница пытается запустить звук только тогда, когда увидела, что остаток стал нулевым.

Сигнал может опоздать или не прозвучать по нескольким причинам:

  • вкладка заморожена или устройство спит;
  • браузер запрещает автоматическое воспроизведение;
  • вкладка, приложение или устройство выключены по звуку;
  • аудиофайл ещё не готов;
  • страница была выгружена.

Пользовательский клик по кнопке запуска повышает шансы, что браузер разрешит аудио. Но разрешение и своевременное выполнение являются разными условиями. Успешно подготовленный аудиоканал всё равно не работает во время сна процессора.

Поэтому интерфейсу полезно давать кнопку проверки звука и показывать ошибку воспроизведения. Молчаливое подавление ошибки создаёт ложное впечатление, будто сигнал обязательно сработает.

Уведомление не превращает страницу в будильник

Браузерное уведомление требует разрешения пользователя и защищённого соединения. Но само по себе оно не содержит команды «доставить через 90 минут»: страница всё равно должна получить возможность выполнить код. Если она заморожена, обычное локальное уведомление не появится точно в срок. Фоновый service worker и серверный push образуют отдельную систему с сетью, разрешениями и собственными задержками, а не простую замену будильнику.

Когда браузерного таймера достаточно

Онлайн-таймер хорошо подходит, когда страница остаётся открытой, допустима небольшая задержка сигнала и пользователь сможет проверить экран: готовка рядом с компьютером, учебный интервал, упражнение или короткая рабочая сессия.

Перед стартом:

  1. Проверьте громкость и тестовый звук.
  2. Не закрывайте вкладку.
  3. Не рассчитывайте на сигнал после сна устройства.
  4. Для длинного интервала поставьте резервный системный будильник.

Для лекарства, отправления транспорта, экзамена, важного созвона или другого события, где минута имеет значение, используйте приложение часов или календарь операционной системы. Они интегрированы с механизмами фонового запуска устройства глубже, чем обычная веб-вкладка.

Надёжная схема проста: хранить конечный момент, использовать интервалы только для перерисовки, сверяться при возвращении и честно описывать ограничения сигнала. Тогда фоновая вкладка может сделать экран менее плавным, но не превращает число вызовов JavaScript в часы.

Есть что добавить?

Напишите своё мнение, комментарий или предложение.