Почему HTML-таймер из чата не будит телефон, и что с этим делать
- среда, 9 сентября 2026 г. в 00:00:05
Знакомый попросил ChatGPT написать ему интервальную тренировку: 40 секунд работа, 20 отдых, восемь кругов, одним HTML-файлом. Чат выдал нормальный код, в браузере всё бегает и пикает. Положил телефон на пол, начал отжиматься — тишина. Экран погас, и вместе с ним погас таймер.
Он спросил у чата «почему», и получил длинный список: PWA с Wake Lock, PWA с заранее склеенной аудиодорожкой (тишина → гонг → тишина → гонг), Capacitor, «соберите APK». Четыре совета, и три из них — обходные манёвры вокруг одного простого факта, который в туториалах обычно не пишут.
Не «пока не умеет», а по решению платформ. Пройдусь по механизмам, в которые все упираются:
setTimeout / setInterval живут, пока жива страница. Вкладка ушла в фон — таймеры троттлятся до раза в минуту, потом страница замораживается целиком. Safari на iOS делает это особенно охотно.
Notifications API умеет показать уведомление, но только пока страница или её service worker бодрствуют. Запланировать «покажи в 10:33» нельзя.
Notification Triggers — была ровно такая спецификация (showTrigger: new TimestampTrigger(t)). Chrome прогнал origin trial в 2020–2021 и закрыл проект. Нигде не вышло и не выйдет.
Service worker просыпается от push с сервера и засыпает через десятки секунд. Своего расписания у него нет.
Web Push работает, в том числе на iOS 16.4+ для PWA на главном экране. Но это сервер: HTTPS, VAPID, подписки, бэкенд, который в нужную секунду шлёт сообщение. Для продукта — нормально. Для таймера на сорок строк из чата — ну такое: чтобы телефон пикнул через 45 минут, нужно поднять и вечно крутить сервер.
Так что классический «будильник на HTML» — <input type="time">, цикл с new Date(), тег <audio> — работает ровно до того момента, как гаснет экран. PWA тут ни в чём не виновата: офлайн, установка на главный экран, кэш — всё это она делает отлично. Просто локальные будильники платформы оставили нативным приложениям.
Если это продукт — Capacitor или натив: плагин Local Notifications даёт настоящие системные будильники. Цена — весь тулчейн: Android Studio, Gradle, подпись, для iOS ещё Xcode и аккаунт разработчика. Для одного личного таймера это неделя вечеров ради функции «пикни в 10:33». А у меня таких файлов из чата уже штук пять: таймер, трекер полива, счётчик подходов. Собирать каждый в отдельный APK не хотелось.
Поэтому я сделал себе штуку попроще: приложение, которое уже собрано, уже умеет системные будильники, и просто запускает мой HTML. Называется Kapsula, есть в обоих сторах, бесплатно. Как устроена песочница и почему в мосте всего пять методов — писал в прошлой статье. Суть такая: вставляешь код (или файл, или URL), он крутится в изолированном iframe, а нативное получает через window.kapsula:
// внутри обычного HTML-файла await kapsula.notify.schedule({ id: 1, at: Date.now() + 45 * 60 * 1000, // через 45 минут title: "Подход 3", body: "45 минут прошло", sound: true });

Это настоящий будильник ОС (AlarmManager с точным таймингом на Android, локальное уведомление на iOS). Звонит секунда в секунду при закрытом приложении и погашенном экране, интернет не нужен, сервера и аккаунта нет в принципе, файл лежит на телефоне. Тот самый код тренировки из первого абзаца я дописал за десять минут: пять строк с schedule и sound.play, остальное как было.
Одна честная оговорка: JavaScript в фоне не исполняется и в раннере. Код заранее отдаёт расписание операционной системе, дальше звонит она, а не ваш setInterval. Отсюда паттерн, который закрывает тренировки, помодоро и всё «пикни каждые N минут»:
экран включён — работает живой JS: setInterval плюс звук, переходов сколько угодно;
приложение свёрнуто или экран погас — на этот случай код при каждом старте и возвращении перепланирует через ОС ближайшие переходы (cancel() → schedule() следующих).
Лимит в Kapsula — 16 запланированных будильников на проект, с перепланированием при каждом открытии этого хватает на сессию любой длины. Чего раннер не умеет, тоже скажу сразу: произвольный код в фоне, голосовые подсказки при погашенном экране, серверные push. Это уже Capacitor. И это не конвертер сайтов: страница со своим бэкендом и куками в песочнице не заживёт.
Веб-страница не заведёт будильник, и это не баг. Web push — не будильник, а сервер и «примерно вовремя».
Продукт — Capacitor или натив. Личный файл из чата — раннер, где будильники уже нативные: kapsula.app (Android и iOS, бесплатно).
Фоновая логика в любом варианте — это расписание, отданное ОС. Кто обещает «JS работает в фоне», проверьте с погашенным экраном.
Справочник моста с примерами и лимитами — kapsula.app/docs. Если знаете ещё живой способ заставить страницу прозвонить при заблокированном экране — напишите в комментариях, мне правда интересно.