navigator.sendBeacon и fetch keepalive: надёжная доставка аналитики без блокировки выгрузки страницы
Как отправлять данные аналитики, которые переживают выгрузку страницы, не задерживая переход — и почему выбор между sendBeacon и fetch keepalive важен для сбора данных без cookie.
Зафиксировать просмотр страницы или событие легко, пока страница активна. Сложность в том, чтобы доставить этот последний payload именно в тот момент, когда пользователь уходит со страницы, закрывает вкладку или сворачивает приложение на мобильном устройстве. Ошибётесь — либо потеряете данные, либо, что хуже, задержите ту самую навигацию, которую запросил пользователь. В этой статье сравниваются два жизнеспособных варианта — navigator.sendBeacon() и fetch() с keepalive: true — и объясняется, какой из них реально использует трекер Monoid и почему, без cookie, хранилищ и фингерпринтинга.
Почему выгрузка — особый случай
Когда документ начинает выгружаться, браузер сворачивает работу. Синхронный XMLHttpRequest исторически злоупотреблялся именно здесь, чтобы блокировать навигацию до завершения запроса, что вредило отзывчивости интерфейса. Современные браузеры активно этому противодействуют, а запросы, начатые во время unload, часто отменяются. Спецификация Beacon существует именно для решения этой проблемы: она позволяет странице запланировать запрос, попытку выполнения которого пользовательский агент гарантирует асинхронно, не блокируя загрузку следующей страницы.
Практический вывод для аналитики: никогда не полагайтесь на обычный fetch или XHR, запущенный из обработчика beforeunload/unload. Он может просто никогда не покинуть машину пользователя.
navigator.sendBeacon
sendBeacon создан именно для этой задачи. Он ставит в очередь небольшой POST-запрос и сразу возвращает булево значение, сообщающее лишь о том, был ли запрос успешно поставлен в очередь, — но не о том, выполнился ли он успешно. Затем браузер передаёт его в фоне, даже после того как страница уже исчезла.
const ok = navigator.sendBeacon('/collect', payload);
Сильные стороны:
- Передача не привязана к времени жизни документа.
- Она имеет низкий приоритет и спроектирована так, чтобы не конкурировать со следующей навигацией.
- Ответ не обрабатывается, поэтому ждать нечего.
Ограничения согласно спецификации и реализациям браузеров:
- Только POST. Произвольные методы задать нельзя.
- Ограниченный контроль над заголовками.
Content-Typeвыводится из типа payload (например,Blobпозволяет на него повлиять, но свободно задать собственные заголовки запроса нельзя). - Payload учитывается в лимите данных пользовательского агента; слишком большие beacon-запросы возвращают
false.
Для аналитики без cookie этого обычно достаточно — множество трекеров именно так и работают и никогда не оглядываются назад. Monoid — нет, по конкретной причине: sendBeacon не может выразить mode: 'cors', а установочный сниппет Monoid позволяет сайту указывать data-api-url на источник, отличный от хост-страницы. Именно это единственное ограничение и решает сравнение ниже.
fetch с keepalive
Стандарт Fetch определяет флаг keepalive, который сохраняет запрос активным даже после завершения жизни инициировавшей его страницы, обеспечивая надёжность на уровне sendBeacon, но с более богатым API.
fetch('/collect', {
method: 'POST',
keepalive: true,
headers: { 'Content-Type': 'application/json' },
body: payload,
});
Сильные стороны:
- Полный контроль над методом, заголовками и телом запроса.
- Настоящий объект
Response, который можно проверить (хотя во время выгрузки его обычно не стоит дожидаться).
Ограничения:
- Спецификация Fetch ограничивает суммарный размер тела всех одновременных keepalive-запросов до 64 КиБ на документ. При превышении лимита fetch отклоняется. Это бюджет на весь документ, так что несколько параллельных keepalive-запросов делят его между собой.
- Исторически поддержка
keepaliveотставала отsendBeacon, и встречались особенности реализации в разных браузерах. Тестируйте на тех браузерах, которыми реально пользуется ваша аудитория.
Именно это трекер Monoid использует для каждого запроса — /collect, /duration и /event одинаково:
fetch(endpoint, {
method: 'POST',
body: JSON.stringify(payload),
headers: { 'Content-Type': 'application/json' },
keepalive: true,
mode: 'cors',
});
Решающий фактор из предыдущего раздела — именно mode: 'cors', а не размер payload или количество заголовков; об этом стоит помнить, поскольку в большинстве материалов этот выбор объясняют исключительно размером.
Что выбрать
Прагматичное правило: используйте sendBeacon для телеметрии по принципу «отправил и забыл», когда ответ вообще не нужен, и обращайтесь к fetch с keepalive только тогда, когда реально нужны собственные заголовки, более крупный структурированный запрос или контроль над mode при межисточниковых запросах. В любом случае держите payload небольшим — значительно меньше лимита keepalive в 64 КиБ, — потому что события аналитики по замыслу должны быть минимальными. Минимизация данных — это не просто требование комплаенса; именно она делает возможной надёжную доставку при выгрузке страницы. Monoid — то самое исключение, которое правило уже учитывает: payload маленький, а ответ никогда не читается — ровно тот профиль, под который создан sendBeacon, — но поддержка mode: 'cors' перевешивает.
На какое событие стоит подписываться
Не полагайтесь на unload. Оно ненадёжно и отключает возможность использования кеша навигации вперёд/назад (bfcache). Рекомендации Page Lifecycle советуют слушать visibilitychange и считать переход в состояние hidden последним надёжным моментом для отправки данных. Это стабильно работает и при закрытии на десктопе, и при переключении вкладок, и при сворачивании приложения на мобильном устройстве — во всех случаях, где unload вообще не срабатывает.
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') flush();
});
Трекер Monoid следует этому подходу: обработчик visibilitychange работает вместе с запасным вариантом на beforeunload, поэтому выборка длительности отправляется независимо от того, свёрнута ли вкладка на мобильном устройстве или полностью закрыта на десктопе.
Как это вписывается в модель без cookie
Поскольку Monoid никогда не читает cookie или хранилища и никогда не занимается фингерпринтингом, каждый запрос несёт лишь то, что и так уже раскрывается, — грубое событие плюс заголовки, которые браузер отправляет в любом случае. Персистентного идентификатора нет, поэтому нам никогда не нужен двусторонний обмен для синхронизации состояния. Именно это делает fetch с keepalive очевидным выбором, как только mode: 'cors' исключает sendBeacon из рассмотрения: одноразовый POST-запрос, ответ которого мы никогда не проверяем, для нас не ограничение — это именно то, что согласует надёжность доставки с принципом privacy by design.
Comments
Loading comments…