12k
All articles

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

Используйте Page Visibility API, чтобы определить, когда вкладка браузера становится неактивной. Останавливайте polling, медиа и аналитику через visibilitychange.

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

У вас есть интервал опроса, срабатывающий каждые десять секунд, видео, проигрывающееся в фоне, или цикл анимации — и всё это потребляет ресурсы для пользователя, который переключился на другую вкладку пять минут назад. Решение очевидно, как только вы узнаете правильный 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?

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

Почему setInterval продолжает срабатывать в фоновой вкладке даже после того, как я определил, что она скрыта?

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

Что использовать для отправки аналитики при уходе пользователя — beforeunload или visibilitychange?

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

Работает ли Page Visibility API внутри iframe?

Да, но поведение зависит от родителя. 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 — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.