Предположим, что у нас есть сайт по продаже билетов, на котором мы хотим в 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.

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

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

Поделиться с друзьями
-->

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


  1. ertaquo
    03.07.2017 14:28
    +2

    Простите, конечно, но на кой черт менять document.title в requestAnimationFrame? Оно вообще не для этого предназначено.
    Насколько мне известно, на текущий момент для подобного нужно использовать Service Workers, если они доступны.


    1. samsdemon
      03.07.2017 14:48

      Никто и не использует, это глупо. Была проблема в том, что у нас новая владка открывается в новом окне, и, если пользователь переходит назад откуда он пришёл, тайтл в новой вкладке не обновляется.

      По факту это было сделано даже не в Service Workers, а просто за пределами requestAnimationFrame, который у нас используется для перехода из состояния «загружается» в состояние «готово».


      1. bano-notit
        04.07.2017 04:36
        +1

        requestAnimationFrame, который у нас используется для перехода из состояния «загружается» в состояние «готово».

        Может я чего-то не допонимаю конечно… Вы его использовали вместо DOMContentLoaded или вместо setImmediate?


        1. samsdemon
          04.07.2017 11:36

          Смотрите. У нас есть страница с заказом места в самолёте, например. Вы переходите на неё со страницы с результатами поиска полётов, выбрав один из них. На этой странице 100500 компонентов, которые обновляются через requestAnimationFrame, чтобы было красиво, а не всем скопом пользователя завалить.

          Как это выглядит в коде


  1. Aingis
    03.07.2017 16:00
    +1

    Всё это было в Симпсонах на MoscowJS: