- PVSM.RU - https://www.pvsm.ru -

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

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

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

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

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

requestAnimationFrame страница

<!DOCTYPE>
<html>

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

<body>tab</body>

</html>

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

Chrome

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

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

IE11

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

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

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

Safari

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

Firefox

Обновление document.title в фоновой вкладке - 4
, где e10s — это Electrolysis [4]

Результат — только 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 [5]. С 2011, Карл!, что означает, что таким поведением никого не удивить. Не удивить, даже если в бэкграунде играет музыка (через <audio src="sound.mp3" autoplay></audio> или что бы то ни было ещё), rAF, в фоне, при любых условиях, в любом браузере, отличным от FF, выполняться не будет.

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

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

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 [7] и setIsVisibleInternal [8].

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

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

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

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

Меняем тайтл на каждый animationFrame

    requestAnimationFrame(function handler() {
      document.title += 'R'
      requestAnimationFrame(handler);
    });

Результат

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

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

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

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

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

Автор: samsdemon

Источник [23]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/259455

Ссылки в тексте:

[1] документации: https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame

[2] http-server: https://www.npmjs.com/package/http-server

[3] lt -p 8080: https://localtunnel.github.io/www/

[4] Electrolysis: https://wiki.mozilla.org/Electrolysis

[5] Per the documentation, Chrome does not call requestAnimationFrame() when a page is in the background. This behavior has been in place since 2011: https://developers.google.com/web/updates/2017/03/background_tabs

[6] исходный код requestAnimationFrame из WebKit: https://github.com/WebKit/webkit/blob/master/Source/WebCore/dom/Document.cpp#L6259

[7] suspendScriptedAnimations: https://github.com/WebKit/webkit/blob/master/Source/WebCore/page/Page.cpp#L1150

[8] setIsVisibleInternal: https://github.com/WebKit/webkit/blob/master/Source/WebCore/page/Page.cpp#L1669

[9] isLowPowerModeEnabled: https://github.com/WebKit/webkit/blob/master/Source/WebCore/page/Page.cpp#L1014

[10] _didReceiveLowPowerModeChange: https://github.com/WebKit/webkit/blob/master/Source/WebCore/platform/ios/LowPowerModeNotifierIOS.mm#L61

[11] topOrigin: https://github.com/WebKit/webkit/blob/master/Source/WebCore/dom/Document.h#L1268

[12] SecurityOrigin: https://lazka.github.io/pgi-docs/WebKit-3.0/classes/SecurityOrigin.html

[13] здесь: https://github.com/WebKit/webkit/blob/ba3ae6914e920af375b07235f3b5c60c427faffe/Source/WebCore/page/SecurityOrigin.cpp#L237

[14] origin: https://developer.mozilla.org/en-US/docs/Web/API/Document/origin

[15] Firefox/Gecko: https://github.com/mozilla/gecko-dev/blob/master/servo/components/script/dom/window.rs#L734

[16] request_animation_frame: https://github.com/mozilla/gecko-dev/blob/master/servo/components/script/dom/document.rs#L1538

[17] is_faking_animation_frames: https://github.com/mozilla/gecko-dev/blob/master/servo/components/script/dom/document.rs#L2481

[18] много : https://github.com/mozilla/gecko-dev/blob/master/servo/components/script/dom/document.rs#L153

[19] Background tabs & offscreen frames — Chrome team future plans: https://docs.google.com/document/d/18_sX-KGRaHcV3xe5Xk_l6NNwXoxm-23IOepgMx4OlE4/pub

[20] requestAnimationFrame Scheduling For Nerds: https://medium.com/@paul_irish/requestanimationframe-scheduling-for-nerds-9c57f7438ef4

[21] VSync if Firefox: http://www.vsynctester.com/firefoxisbroken.html

[22] How to dramatically improve Chrome's requestAnimationFrame VSYNC accuracy in Windows: https://bugs.chromium.org/p/chromium/issues/detail?id=467617

[23] Источник: https://habrahabr.ru/post/332174/?utm_source=habrahabr&utm_medium=rss&utm_campaign=sandbox