javascript

Обновление document.title в фоновой вкладке

  • вторник, 4 июля 2017 г. в 19:11:43
https://habrahabr.ru/post/332174/
  • JavaScript


Предположим, что у нас есть сайт по продаже билетов, на котором мы хотим в document.title показывать цену, чтобы увеличить удовлетворённость пользователей и дать им знать, что в какой вкладке у них открыто (т.к. мы не знаем, будет ли меняться место отправления/прибытия, дата, любимый перевозчик и так далее, а цена, напротив, для различных комбинаций практически всегда разная). При этом мы знаем, что, начиная с версии 56, Chrome проводит политику блокировки фоновых вкладок. Что это означает для данного функционала?

Согласно документации, rAF (requestAnimationFrame) обычно вызывается 60 раз в секунду. Для экранов, у которых частота обновления выше, это значение будет также выше (хотя вы можете и не замечать это глазом).

Далее написано, что, если вкладка фоновая и не активная, то частота может уменьшиться, что и происходит, как мы увидим ниже.

Проведём эксперимент. В коде под спойлером по requestAnimationFrame простейшим образом обновляется тайтл:

requestAnimationFrame страница
<!DOCTYPE>
<html>

<head>
  <script>
    requestAnimationFrame(function() {
      document.title += ' RAF'
    });
  </script>
  <title>tab</title>
</head>

<body>tab</body>

</html>


Результат в хроме выглядит следующим образом:

Chrome


Да, так и должно быть. Давайте посмотрим, что делают другие браузеры:

IE11
Чтобы не ходить в настройки и не разрешать выполнение скриптов на локальных сайтах, воспользуемся комбинацией команд http-server и lt -p 8080:



* Edge работает аналогично

Safari


Firefox

, где e10s — это Electrolysis

Результат — только Firefox выполняет rAF в фоновой вкладке. Казалось бы удивительно, что даже IE11 не выполняет rAF в фоне, однако, Per the documentation, Chrome does not call requestAnimationFrame() when a page is in the background. This behavior has been in place since 2011. С 2011, Карл!, что означает, что таким поведением никого не удивить. Не удивить, даже если в бэкграунде играет музыка (через <audio src="sound.mp3" autoplay></audio> или что бы то ни было ещё), rAF, в фоне, при любых условиях, в любом браузере, отличным от FF, выполняться не будет.

Поэтому, отвечая на изначальный вопрос «Как показать цену в тайтле», ответ достаточно прост — показывать её не в момент отображения компонента, а в момент получения данных из API. Вторым решением был бы вынос из функции, которая используется для анимации страницы при возврате данных с API (большой лоадер на весь экран красиво убираем просто)), функции-инициализатора. К сожалению, так сложилось, что таких функций почему-то больше чем одна, поэтому этот вариант влечёт за собой большие перестановки и от него решено было отказаться. К слову, setTimeout/setInterval, выполняются корректно, через них возможен третий вариант решения проблемы.

На этом стори можно доделывать и закрывать, а теперь перейдём к тому, что происходит на самом деле, используя исходный код requestAnimationFrame из WebKit:

int Document::requestAnimationFrame(Ref<RequestAnimationFrameCallback>&& callback)
{
    if (!m_scriptedAnimationController) {
#if USE(REQUEST_ANIMATION_FRAME_DISPLAY_MONITOR)
        m_scriptedAnimationController = ScriptedAnimationController::create(*this, page() ? page()->chrome().displayID() : 0);
#else
        m_scriptedAnimationController = ScriptedAnimationController::create(*this, 0);
#endif
        // It's possible that the Page may have suspended scripted animations before
        // we were created. We need to make sure that we don't start up the animation
        // controller on a background tab, for example.
        if (!page() || page()->scriptedAnimationsSuspended())
            m_scriptedAnimationController->suspend();

        if (page() && page()->isLowPowerModeEnabled())
            m_scriptedAnimationController->addThrottlingReason(ScriptedAnimationController::ThrottlingReason::LowPowerMode);

        if (!topOrigin().canAccess(securityOrigin()) && !hasHadUserInteraction())
            m_scriptedAnimationController->addThrottlingReason(ScriptedAnimationController::ThrottlingReason::NonInteractedCrossOriginFrame);
    }

    return m_scriptedAnimationController->registerCallback(WTFMove(callback));
}

Первый if довольно понятен, подробнее по suspendScriptedAnimations и setIsVisibleInternal.

Во втором if, значение функции isLowPowerModeEnabled, у каждой ОС будет браться из разных источников; в iOS, вероятно, оно берётся из _didReceiveLowPowerModeChange.

Третий if наиболее интересен; topOrigin это просто SecurityOrigin документа верхнего уровня; сам метод находится здесь, в комментариях в этом методе достаточно подробно расписано, что в нём происходит. Этот метод возвращает true, если два скрипта с разных origin могут общаться между собой (на чтение или на запись). В то же время, у пользователя остаётся место для манёвра при помощи NonInteractedCrossOriginFrame.

Таким образом, webkit-браузеры будут получать блокировку rAF каждый раз, когда они заходят в один из трёх if.

Вернёмся к Firefox. Преобразуем исходную функцию, меняющую заголовок страницы, на следующую:

Меняем тайтл на каждый animationFrame
    requestAnimationFrame(function handler() {
      document.title += 'R'
      requestAnimationFrame(handler);
    });


Результат


Как можно заметить, в Firefox/Gecko всё происходит немного иначе, чем в webkit. Если найти реализацию request_animation_frame, то можно заметить, что:

  1. // TODO: Should tick animation only when document is visible — что означает, что когда-нибудь поведение будет соответствовать поведению остальных браузеров, поэтому на том, что происходит сейчас, можно особо не зацикливаться.
  2. Существует is_faking_animation_frames где оказывается, что ребята из Gecko, в случае, если у вас много фейковых анимаций, выбрасывают вас на FakeRequestAnimationFrameCallback.

Даже не знаю, кто круче.

Хорошие ссылки: