хабраиндекс
90,25
8 августа 2011 в 18:10

Page Visibility API и Visibility.js

Кот Шрёдингера

Page Visibility API — новое API в JavaScript, которое позволяет узнать, видит ли пользователь ваш сайт или же он, например, открыл другой таб.

Каким образом это API может сделать наш Веб дружелюбнее и уютнее? Ну самое очевидное:
  • Сделать сайт более дружелюбным к пользователю, «поднять юзабилити». Например, отключать слайдшоу или ставить видео на паузу, когда вы переключаетесь в другой таб (например, вы смотрите видео на YouTube и вам приходит срочное эл. письмо).
  • Не потреблять лишних ресурсов. Выключать лишнюю логику, когда она не нужна, так как пользователь не видит сайт. Например, в фоновом табе отключать сложные JS-рассчёты или реже проверять новые сообщения по AJAX.
  • Считать более точную статистику. Например, не засчитывать пользователей, которые открыли ваш сайт в новом табе и закрыли его не просматривая.
  • Поддерживать новую технологию пререндеринга из Google Chrome, когда браузеру заранее загружает и рендерит указанную страницу, чтобы открыть её мгновенно. Например, в поиске Google первый результат выдачи будет отмечен на прередеринг.
  • Сделать эмулятор кота Шрёдингера (на иллюстрации), который отобразит живого или мёртвого кота только тогда, когда пользователь откроет загруженный в фоне таб.

Чтобы сделать работу с Page Visibility API более удобной, я (во славу Злых марсиан) разработал библиотеку Visibility.js. Она позволяет забыть о вендорных префиксах и добавляет «сахара» высокоуровневых функций, чтобы писать короткий чистый код (например, Visibility.every — аналог setInterval, но работает только, если сайт в открытом табе).

Милый пример видео-проигрывателя, который останавливает видео, когда страница становится невидимой (открывать в Google Chrome 13).

Поддержка в браузерах


Page Visibility API уже сейчас поддерживается в Google Chrome 13 и IE 10. Для Firefox 5 и выше есть хак MozVisibility от private_face, который эмулирует Page Visibilty API (этот хак надо подключать в странице перед Visibility.js).

Уже есть черновик стандарта Page Visibility API от W3C, так что поддержка в других браузерах — вопрос времени.

Но совсем не обязательно, чтобы браузеры всех ваших пользователей поддерживали этот API — это просто улучшение, а не добавление нового функционала, как тег <video>. Если поддержка есть — пользователю будет удобнее, если нет — сайт будет работать как обычные сайты и думать, что пользователь всегда видит сайт. Высокоуровневые функции в Visibility.js специально так сделаны, чтобы разработчик мог не задумываться, есть ли поддержка API или нет.

Состояния


Сейчас в стандарте есть 4 возможных состояния видимости страницы:
  • visible — пользователь сейчас видит страницу.
  • hidden — страница не видна для пользователя, так как в браузере открыт другой таб, окно браузера свёрнуто или ОС вообще заблокировало экран. Правда в реальности Chrome проверяет только открыт ли текущий таб или нет, сворачивание браузера никак не влияет.
  • prerender — браузер загрузил и отрендерил страницу заранее, чтобы потом мгновенно показать пользователю. То есть, сейчас пользователь страницу не видит, лишние вычисления и мультимедиа надо убрать, а в статистике такой просмотр пока не считать. Вся технология поддерживается только в Google Chrome, хотя уже есть в стандарте.
  • preview — сайт открыт в маленьком окне предпросмотра. Например, в мозайке часто открываемых сайтов в новом табе. Теоретическое свойство из стандарта, так как пока не поддерживается ни одним браузером.

А что будет, когда в стандарт добавят ещё одно состояние, а вам нужно будет проверить просто видим ли сайт для пользователя или нет. Для этого есть свойство document.hidden (не забывайте про вендорные префиксы, в Chrome оно будет document.webkitHidden) или метод Visibility.hidden() в Visibility.js. Если вам нужно проверить, видим ли сайт — используйте именно это свойство, а не сравнивайте название состояния с "hidden".

Visibility.js


Чтобы не пугать низкоуровневым кодом с кучей проверок на вендорные префиксы, я буду показывать работу с Page Visibility API сразу на примере Visibility.js, а в конце статьи расскажу и об низкоуровневых методов из стандарта API.

Код библиотеки полностью покрыт модульными тестами, отдокументирован и библиотека уже успешно используется на русском Групоне. В сжатой версии она весит всего 1 КБ, зато избавляет вас от кучи лишнего кода.

Visibility.every

Visibility.every(interval, callback) — это аналог setInterval(callback, interval), но запускает callback, только если пользователь сейчас видит страницу.

Например, будем показывать анимацию обратного отсчёта, только когда пользователь видит страницу:
Visibility.every(1000, function() {
    updateCountdownAnimation();
});


Visibility.every(visible, hidden, callback) может принимать 2 интервала времени — visible будет использовать, когда страница видима для пользователя, а hidden — когда скрыта.

Например, можно проверять новые сообщения каждую минуту, когда пользователь открыл сайт (то есть ему это важно). А когда пользователь не видит сайт (читает другую страницу) будем экономить его трафик и проверять почту каждые 5 минут:
var minute = 60 * 1000;
Visibility.every(minute, 5 * minute, function () {
    checkForEmail();
});


Но вообще указывать время в милисекундах — издевательство над человеком. Поэтому Visibility.js поддерживает интеграцию с jQuery Chrono plugin (его нужно просто подключить перед Visibility.js). Код становится понятным и мило засахаренным:
Visibility.every('minute', '5 minutes', function () {
    checkNewMails();
});


Чтобы остановить таймер, запущенный с помощью Visibility.every, нужно использовать Visibility.stop(timerID) (clearInterval работать не будет):
var slideshow = Visibility.every(5 * 1000, function () {
    nextSlide();
});

$('.stopSlideshow').click(function () {
    Visibility.stop(slideshow);
});


Если браузер не поддерживает Page Visibility API, то Visibility.every будет считать, что пользователь всегда видит сайт (то есть она становится полным аналогом setInterval, но ничего страшного не происходит).

Visibility.onVisible

Другая стандартная ситуация — когда мы ждём, пока пользователь не увидит сайт (например, потому что открыл ссылку в фоновом табе). Для этого есть метод Visibility.onVisible(callback), который выполняет код только тогда, когда страница становится видимой. Если страница уже видима, то callback вызовется сразу же.

Например, при посещении сайты мы показываем какое-то уведомление и через 10 секунд красиво скрываем его. Но если пользователь откроет сайт сразу в фоновом табе, то он может пропустить уведомление. Давайте тогда отсчитывать 10 секунд после того, как пользователь действительно увидит сайт:
Visibility.onVisible(function () {
    setTimeout(function() {
        Notification.hide();
    }, 10 * 1000);
});


Если браузер не поддерживает Page Visibility API, то Visibility.onVisible(callback) вызовет callback сразу же.

Visibility.afterPrerendering

У Firefox есть rel="<a href="https://developer.mozilla.org/en/Link_prefetching_FAQ">prefetch</a>" для ссылок, который говорит браузеру, что пользователь скорее всего откроет потом эту ссылку, так что браузер заранее загружает её содержимое. Это нужно, например, чтобы загрузить следующую главу статьи или для первого результата в поисковой выдаче.

Google Chrome пошёл дальше и сделал rel="<a href="http://code.google.com/chrome/whitepapers/prerender.html">prerender</a>" — он не только загружает, но и рендерит страницу заранее, чтобы открыть её мгновенно (видео примера от Google).

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

Для всех этих задач есть Visibility.afterPrerendering(callback), который выполнит callback, только когда страница откроется по настоящему (то есть выйдет из состояния пререндеринга) или выполнит callback сразу же, если страница сразу была нормально открыта. В callback можно включить автообновление через AJAX, добавить на страницу <video> и посчитать пользователя в статистике.

Visibility.afterPrerendering(function () {
    Statistics.countVisitor();
});


Если браузер не поддерживает Page Visibility API или пререндеринг, то Visibility.afterPrerendering(callback) вызовет callback сразу же.

Низкоуровневые функции

Если вы хотите понять, на основе чего работает весь «сахар» из примеров выше, или вы сделать что-то более сложное, то вам понадобятся низкоуровневые функции Visibility.js. Тут же я покажу, как Page Visibility API работает.

Visibility.isSupported() возвращает true, если браузер поддерживает Page Visibility API. Браузер не поддерживающий Page Visibility API можно легко узнать — у него document.hidden будет undefined, а не true или false (только надо не забывать про вендорный префикс, например, document.webkitHidden).

Visibility.state() возвращает имя состояния ("visible", "hidden" или "prerenderer"). Этот метод просто смотрит в свойство document.visibilityState с учётом вендорного префикса (например, document.webkitVisibilityState). Небольшой пример для закрепления:
if( Visibility.isSupported() ) {
    if ( 'hidden' == Visibility.state() ) {
        Statistics.userOpenPageInBackgroundTab();
    }
    if ( 'prerender' == Visibility.state() ) {
        Statistics.pageIsPrerendering();
    }
})


Если нужно просто проверить, видит ли пользователь страницу или нет, то лучше использовать Visibility.hidden() (так как список состояний может в дальнейшем пополниться). Она просто смотрит в свойство document.hidden. Следующий пример включает автопроигрывание видео, только если страница открылась сразу в активном табе (а не в новом фоновом):
$(document).load(function () {

   if ( !Visibility.hidden() ) {
       VideoPlayer.play();
   }

});


Чтобы следить за изменением состояния страницы, у document есть событие visibilitychange (в Chrome — webkitvisibilitychange). В Visibility.js есть более короткий способ — метод Visibility.change(callback) сам вешает обработчик события и вызывает callback при каждой смене видимости страницы. Первый аргумент у callback будет объект события, а второй — имя состояния. Пример:
Visibility.change(function (e, state) {
    Statistics.trackChangeVisibility(state);
});


Установка

  • Если вы используете Ruby on Rails 3.1, то вам проще всего. Подключите visibilityjs гем в Gemfile:
    gem 'visibilityjs'

    и подключите библиотеку в app/assets/javascripts/application.js.coffee:
    #= require visibility

  • Если вы используете какой-то сборщик статики, например, Jammit, то скачайте lib/visibility.js в public/javascripts/lib вашего проекта и подключите Visibility.js в настройках пакетов в config/assets.yml:
    javascripts:
      application:
        - public/javascripts/lib/visibility.js
    
  • Если вы почему-то не думаете о клиентской оптимизации и не объединяете все JS-файлы проекта для последующего сжатия, то можете скачать уже сжатую версию Visibility.js — visibility.min.js.

Ссылки


+93
4236
212
Iskin 75,9 G+

Комментарии (49)

+7
Scorpil, #
А еще благодоря этой библиотеке можно делать нехорошие вещи, например, пока пользователь не видит, перезагрузить содержимое таба, чтобы страница выглядила как PayPal. Пользуйтесь NoScript…
+1
5erg, #
майнер бит-бабок запустить, например… Ещё идеи? :-)
0
variable, #
тупо ддосить)
+1
Zibx, #
Слишком малый выхлоп, пробовали на час запускать на сайте с 2к уников в день. Потом пробовали ява майнер — тоже бесполезно.
+8
int03e, #
Что мешает сразу рендерить фишинговую страницу? Что-то юзкейса не понял…
+2
NekitoSP, #
ну как это… допустим ты читал новости на сайте N, и отправлял бабло в PayPal'e, затем тебе пришло письмо, пока ты его читал — страничка на сайте N подменилась, подменилась и иконка, и ты, ничего не подозревая возвращаешься в «псевдо» PayPal и как ни в чем ни бывало пытаешься снова залогиниться, ан-нет, не получится, ведь это ненастоящий PayPal :) примерно это имел ввиду Scorpil в своем сообщении выше.
+1
int03e, #
Сертификат тоже «подменился»? :-) Хотя да, если с этой точки зрения рассматривать, то атака возможна… очень специфическая и сомнительная.
0
NekitoSP, #
ну о каком еще сертификате может идти речь, если вы 2 минуты назад были на пайпале, и почитав письмо, вернулись на него. Какой еще сертификат, что же вы смешите?) да вы на строку браузера наврядли взглянете то, не говоря уже о составе страницы, и текущем протоколе (http/https).
0
Iskin, #
Вы не вернётесь — вы читали в другом табе же. Вам нужно вручную переключиться на другой таб. Тем более надо очень точно подгадать момент.
+1
NekitoSP, #
самый момент — когда табов много. Если вы не знакомы с таким — то вам меня наверное не понять :)
+1
NekitoSP, #
… И это уже будет именно психологический фактор, т.к. фактически вы и не были знакомы ранее с такими способами обмана, и не подозревали о таком. Ответьте мне на вопрос пожалуйста, смОтрите ли вы на строку браузера, выбирая в списке табов лишь нужный таб по иконке/заголовку и далее после клика — по содержимому страницы?
0
NekitoSP, #
небольшое уточнение: "… смОтрите ли вы на адресную строку браузера..."
0
int03e, #
Учитывая то, что строка находится сразу под табами — на нее тяжело не взглянуть, а уж тем более не заметить то, что url разительно отличается от paypal.com. Плюс сертификат — не проверить есть ssl или нет перед транзакцией это просто тупо. Хотя наверняка есть люди которые этого не делают, не спорю :-)
0
JC_Piligrim, #
Кстати, как к этому относятся концепт-дизайнеры Chrome и FF? Ведь, они вообще собираются убрать адресную строку из браузера.
0
Iskin, #
Строка будет появлятся при наведении на табы и при их смене.
0
Iskin, #
Единственное нарушение безопасности, которое сейчас выглядит адекватным — это сбор большей статистики, о том, как вы пользуетесь сайтом. Плюс, иногда можно понять, что это один и тот же пользователь просматривает два сайта — если он будете переключать из одного сайта в другой (но есть более простые способы это узнать) — в общем проблем с безопасностью я тут пока не вижу.
0
NekitoSP, #
«Единственное нарушение безопасности, которое сейчас выглядит адекватным — это сбор большей статистики, о том, как вы пользуетесь сайтом»

То что многие веб-разработчики и сами иногда не прочь поставить всякие лайвинторнеты, яндекс-метриксы, и прочие счетчики на свой сайт, это ничего, а то что какой-то сайт тупо собирает вашу активность/неактивность в данный момент времени — это угроза безопасности?
Не согласен с вами. Тот же лайвинтернет соберет о Вас гораздо больше инфы, чем статистика активности/неактивности, так что не вижу повода беспокиться.
0
shergin, #
Новопассит поможет тоже!
+3
kosiakk, #
Visibility.onVisible имеет тот же порядок аргументов что и setInterval (функция, интервал), а Visibility.every — прямо противоположный.

Злые вы, марсиане.
+2
Iskin, #
У Visibility.onVisible только один аргумент — callback. В примере я вызываю обычный setTimeout.
–2
Iskin, #
У Visibility.every порядок аргументов такой же, как у jQuery.Chrono — потому что это очень удобный и человечный синтаксический сахар :). Объявим бой C-трешу из JS! ^_^
+2
Rathil, #
Удобная вещь, можно четко определить читает ли твой контент пользователь или нет.
0
Iskin, #
В такой задачи, кстати, лучше следить за перемещениями курсора мышки и скролла :). А Page Visibility API может сказать, например, слушаюи ли вашу музыку в фоне или держать окно открытым.
0
gryzzly, #
для того, чтобы анимации не жраил процессор нужно пользоваться requestAnimationFrame, но для других вещей — это очень крутой API, спасибо :-)
+2
Iskin, #
Верно :). Хотя иногда анимация не бесконечное циклична, а её нужно запускать, например, раз в 1 с (как обратный отсчёт или слайдшоу) — тут requestAnimationFrame не так идеальный — лучше объединить с Page Visibility API.
–1
datacompboy, #
Прощай, заработок с popUnder'ов!
0
gigigi, #
Не знаю было ли так задумано или нет или может это не входит в возможности API, но при переключении на другое окно (нажал на уведомление из thunderbird во время просмотра видео) оно не понимает, что я больше не вижу этот таб
+1
Iskin, #
Да, я написал, что Chrome пока следит только за переключениями табов. В стандарте же указано, что hidden — это и сворачивание окна браузера или же блокировка экрана ОС.
0
gigigi, #
А, извините — не заметил.
+8
lukaville, #
Теперь всякие летитбиты и депозиты будут следить за тем, смотришь ли ты на страницу, и если нет, то останавливать счетчик…
+7
tendium, #
Не просто будут. Уже следят! Я сегодня наткнулся на такой файлопомойный сайт. Пока понял это — минут 10 прошло. Я ввожу капчу, запускается счетчик, я сваливаю. Через минуту захожу — никаких ссылок на скачку нет. Думаю, черт, глюк, ввожу капчу снова. Снова сваливаю. Фэйл повторяется. Потом я стал переключаться туда-сюда и заметил, что счетчик-то не бежит, когда я в другой закладке. Пришлось сидеть, ждать. Вот ведь грустно-то как :)
+3
Iskin, #
Да, это ужасная тёмная сторона этой технологии. В слабой попытке защититься хочу сказать, что есть хаки, определяющие что вы в другой табе и без Page Visibility API. Например хак MozVisibility использует то, что браузеры в фоновых табах запускают steInterval не чаще раза в секунду.
+1
egorinsk, #
Это неправильное API (впрочем, в HTML/CSS/JS столько недочетов и недоработок, что это уже мелочи). По идее, при сворачивании окна/переключении с таба браузер должен по умолчанию полностью его останавливать, чтобы не расходовать ресурсы. Если какому-то табу нужно быть активным и в фоне, он должен использовать какие-нибудь специальные функции для этого, чтобы попроить браузер не отключать его.

А такой подход — отключать анимацию и вычисления при уходе с таба по событию — работать не будет, так как 95% разработчиков просто не будут с этим заморачиваться.

И, кстати, подобные штуки давно уже используются (вроде бы) в Гуглокартах и вконтакте — они определяют активность пользователя по событиям мыши и клавиатуры и как-то на ее отсутствие реагируют.
0
Iskin, #
Ваш метод слишком жесток — достаточно просто сильно ограничивать фоновые табы. Например, все нормальные браузеры сейчас запускаю setInterval-код минимум раз в секунду.
+3
Iskin, #
Впрочем, Page Visibility API — это не только об ограничении ресурсов. Это в первую очередь об юзабилити (вот бы YouTube начал его поддерживать и не включал бы видео, если оно открывается сразу в фоновом табе).
+5
artishok, #

<html>
	<head>
		<title>тайтл</title>
		<script>
			window.onblur = function () {document.title='документ неактивен'}
			window.onfocus = function () {document.title='документ снова активен'}
		</script>
	</head>
	<body>
		<img src="http://www.google.com.ua/images/nav_logo83.png">
	</body>
</html>


http://ssms-ka.narod2.ru/index_onblur.html
0
Iskin, #
Прекрасный хак!
0
Iskin, #
А он будет работать, если в body будет форма и сложное оформление?
0
artishok, #
Вот с формами
http://ssms-ka.narod2.ru/Habrahabr.htm
0
Iskin, #
Да, отлично работает. Но логика его работы всё равно отличается от Page Visibility API :(.
0
Iskin, #
Хотя он становится неактивным, даже если окно браузера просто теряет фокус, хотя сайт всё ещё видим пользователю.
0
tibalt, #
причем способ стар как мир и успешно применяется.
0
spmbt, #
Метод определения открытого таба или видимого участка страницы существовал давно, ещё при IE5-6 и для всех браузеров. Но решение было некрасивое — нагружать периодически браузер анимацией на 100% загрузки процессора и сравнивать время. За пределами окна и при закрытом табе время было больше для той же анимации. И если 2 таба решат действовать таким способом, показания будут влиять друг на друга. Из-за некрасивости метода никогда не было желания довести его до конца, до некоторой утилиты. Но счётчикам на странице некрасивость — не помеха; странно, что они только сейчас начинают внедрять детектирование открытого таба.
0
Iskin, #
Так примерно MozVisibility и работает — только он строится на довольно сильном снижении периода запусков setInterval.
+1
ykrop, #
ради высшей справедливости, реализовать симулятор кота Шрёдингера в браузере нельзя, ведь сам опыт показывает парадокс неопределенности состояния элементарной частицы. только имитатор. (вот если бы подключить к компьютеру какой-нибудь USB-счетчик Гейгера то будет совсем другое дело!)

а симулятор подбрасывания монетки — можно.
0
ttas, #
Например, можно проверять новые сообщения каждую минуту, когда пользователь открыл сайт (то есть ему это важно). А когда пользователь не видит сайт (читает другую страницу) будем экономить его трафик и проверять почту каждые 5 минут:
Я могу уйти со страницы (вкладки), при этом для меня важность получения сообщения или почты не уменьшится.
0
kuchumovn, #
У меня в хроме не работает test/integration.html: выдаёт hidden, и потом уже не меняет, куда бы я ни сворачивал/тыкал/переключал.
Пробовал сам написать на голом API — тоже ничего не работает.
Видимо сломали.
Возвращаюсь к window.focus и window.blur.
0
Iskin, #
Добрый день. Я прямо сейчас проверим в свежей бете Хрома (23.0.1271.26) — всё работает.

Если бы Page Visibility API был бы отключён, то страница выдавала бы «visible», а не «hidden» (по-умолчанию страница видима), так что я думаю, что API работает, просто посредине процесса происходит какая-то ошибка.

Вы можете посмотреть лог (Ctrl + Shift + J) — нет ли там сообщения об ошибке.
0
kuchumovn, #
Не, нету ничего.
Да ладно, пусть пока обкатают нормально, потом буду использовать.

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