Back

Как определить, что вкладка браузера стала неактивной

Как определить, что вкладка браузера стала неактивной

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

Ключевые выводы

  • Page Visibility API — это современный и надёжный способ определять изменения видимости вкладки, заменяющий устаревшие подходы с window.onfocus и window.onblur.
  • Используйте document.hidden или document.visibilityState вместе с событием visibilitychange, чтобы реагировать на переход вкладки в неактивное или активное состояние.
  • Практические применения включают приостановку опросов, остановку воспроизведения медиа, автосохранение черновиков форм и отправку аналитики при завершении сессии.
  • Сочетайте visibilitychange с pagehide и navigator.sendBeacon() для надёжного отслеживания завершения сессии и избегайте события unload, поскольку оно ломает back/forward cache.
  • Всегда удаляйте слушатели при размонтировании компонентов в одностраничных приложениях, чтобы предотвратить утечки памяти.

Правильный инструмент: Page Visibility API

Старые решения опирались на события focus и blur объекта windowwindow.onfocus и window.onblur. Они определяют, есть ли у окна браузера фокус клавиатуры, а не действительно ли вкладка видима. В режиме разделённого экрана или при работе с несколькими окнами вкладка может быть полностью видимой, не имея при этом фокуса, и наоборот. Кроме того, их поведение при переключении вкладок было непоследовательным в разных браузерах.

Page Visibility API решает эту задачу корректно. Он точно сообщает, видима ли страница пользователю, независимо от состояния фокуса.

Текущее состояние можно узнать с помощью двух свойств:

  • document.visibilityState — возвращает "visible" или "hidden"
  • document.hidden — возвращает true, когда вкладка скрыта, и false, когда видима

В современных браузерах вендорные префиксы не нужны. API имеет широкую поддержку в Chrome, Firefox, Safari и Edge — как на десктопе, так и на мобильных устройствах.

Прослушивание события visibilitychange

Событие visibilitychange срабатывает на document всякий раз, когда вкладка становится скрытой или снова видимой. Базовый паттерн выглядит так:

document.addEventListener("visibilitychange", () => {
  if (document.hidden) {
    console.log("Tab is hidden — pause or reduce work");
  } else {
    console.log("Tab is visible — resume work");
  }
});

Вы также можете напрямую читать document.visibilityState, если вам нужно строковое значение, а не булево.

Практические сценарии использования

Приостановка и возобновление опроса

Вместо того чтобы отправлять API-запросы, пока пользователь находится на другой вкладке, приостановите интервал и возобновите его при возвращении:

let intervalId = null;

function startPolling() {
  if (intervalId !== null) return;
  intervalId = setInterval(fetchData, 10000);
}

function stopPolling() {
  clearInterval(intervalId);
  intervalId = null;
}

document.addEventListener("visibilitychange", () => {
  document.hidden ? stopPolling() : startPolling();
});

startPolling();

Обратите внимание: браузеры тротлят setInterval в фоновых вкладках, а не полностью останавливают его, поэтому полагаться на точность таймингов в скрытых вкладках в любом случае ненадёжно. Явная приостановка чище. Защита от дублирующих интервалов также предотвращает наложение нескольких таймеров, если startPolling будет случайно вызван дважды.

Приостановка воспроизведения медиа

const video = document.querySelector("video");

document.addEventListener("visibilitychange", () => {
  if (document.hidden) {
    video.pause();
  } else {
    video.play().catch(() => {
      // Autoplay may be blocked by the browser; handle silently
    });
  }
});

Обработчик .catch() важен, поскольку video.play() возвращает промис, который может быть отклонён, если браузер блокирует автовоспроизведение — иначе это привело бы к необработанной ошибке.

Автосохранение черновика формы

Запускайте сохранение, когда пользователь покидает вкладку, а не по фиксированному таймеру:

document.addEventListener("visibilitychange", () => {
  if (document.hidden) {
    saveDraft();
  }
});

Отправка аналитики перед уходом пользователя

Для данных о завершении сессии комбинируйте событие visibilitychange с pagehide и используйте navigator.sendBeacon() для надёжной отправки данных без блокировки навигации:

document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "hidden") {
    navigator.sendBeacon(
      "/analytics",
      JSON.stringify({ event: "tab_hidden" })
    );
  }
});

window.addEventListener("pagehide", () => {
  navigator.sendBeacon(
    "/analytics",
    JSON.stringify({ event: "page_unload" })
  );
});

Избегайте использования события unload для этой цели — оно ненадёжно в современных браузерах и ломает back/forward cache (bfcache), что снижает производительность навигации. В мобильных браузерах visibilitychange со состоянием "hidden" часто является единственным надёжным сигналом завершения сессии, так как pagehide и beforeunload срабатывают не всегда.

Очистка слушателей событий

Всегда удаляйте слушатели, когда они больше не нужны, особенно в одностраничных приложениях:

function handleVisibility() {
  console.log(document.visibilityState);
}

document.addEventListener("visibilitychange", handleVisibility);

// Later, when tearing down:
document.removeEventListener("visibilitychange", handleVisibility);

Помните, что ссылка, передаваемая в removeEventListener, должна быть точно той же ссылкой на функцию, что использовалась в addEventListener. Встроенные стрелочные функции удалить нельзя, поскольку каждая создаёт новую ссылку.

Заключение

Page Visibility API — а именно document.hidden, document.visibilityState и событие visibilitychange — это корректный и современный способ определять, когда вкладка браузера становится неактивной. Он надёжно работает во всех актуальных браузерах, точно обрабатывает переключение вкладок там, где события focus и blur объекта window оказываются недостаточны, и хорошо сочетается с navigator.sendBeacon() и событием pagehide для обработки завершения сессии. Используйте его, чтобы остановить ненужную работу, сохранить состояние или отслеживать вовлечённость, и не забывайте очищать слушатели по завершении.

Часто задаваемые вопросы

Оба сообщают, видима ли страница, но возвращают разные типы. document.hidden возвращает булево значение, что удобно для быстрых условных проверок. document.visibilityState возвращает строку — в настоящее время либо visible, либо hidden — что более описательно и устойчиво к будущим изменениям, если будут добавлены дополнительные состояния. Используйте то, что лучше вписывается в стиль вашего кода.

Браузеры тротлят фоновые таймеры, а не останавливают их, обычно ограничивая частоту срабатывания до одного раза в секунду или реже. Интервал продолжает работать, но с непредсказуемой частотой, поэтому логика, чувствительная к таймингам, становится ненадёжной. Самое чистое решение — вызвать clearInterval внутри обработчика visibilitychange и перезапустить интервал, когда вкладка снова станет видимой.

Предпочтительнее visibilitychange в сочетании с pagehide и navigator.sendBeacon. События beforeunload и unload ненадёжны на мобильных устройствах и ломают back/forward cache, ухудшая производительность. Событие visibilitychange со состоянием hidden — наиболее последовательный сигнал на десктопных и мобильных браузерах для определения того, что пользователь покинул страницу.

Да, но поведение зависит от родителя. Iframe, как правило, наследует состояние видимости родительского документа. Скрытие самого iframe через CSS, например `display: none`, не вызывает событий `visibilitychange` внутри iframe. У каждого iframe свой объект document, поэтому слушать visibilitychange нужно на документе iframe, а не на верхнем окне.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay