Предположим, что у нас есть сайт по продаже билетов, на котором мы хотим в document.title показывать цену, чтобы увеличить удовлетворённость пользователей и дать им знать, что в какой вкладке у них открыто (т.к. мы не знаем, будет ли меняться место отправления/прибытия, дата, любимый перевозчик и так далее, а цена, напротив, для различных комбинаций практически всегда разная). При этом мы знаем, что, начиная с версии 56, Chrome проводит политику блокировки фоновых вкладок. Что это означает для данного функционала?
Согласно документации, rAF (requestAnimationFrame) обычно вызывается 60 раз в секунду. Для экранов, у которых частота обновления выше, это значение будет также выше (хотя вы можете и не замечать это глазом).
Далее написано, что, если вкладка фоновая и не активная, то частота может уменьшиться, что и происходит, как мы увидим ниже.
Проведём эксперимент. В коде под спойлером по requestAnimationFrame простейшим образом обновляется тайтл:
<!DOCTYPE>
<html>
<head>
<script>
requestAnimationFrame(function() {
document.title += ' RAF'
});
</script>
<title>tab</title>
</head>
<body>tab</body>
</html>
Результат в хроме выглядит следующим образом:
Да, так и должно быть. Давайте посмотрим, что делают другие браузеры:
* Edge работает аналогично
, где 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. Преобразуем исходную функцию, меняющую заголовок страницы, на следующую:
requestAnimationFrame(function handler() {
document.title += 'R'
requestAnimationFrame(handler);
});
Как можно заметить, в Firefox/Gecko всё происходит немного иначе, чем в webkit. Если найти реализацию request_animation_frame, то можно заметить, что:
- // TODO: Should tick animation only when document is visible — что означает, что когда-нибудь поведение будет соответствовать поведению остальных браузеров, поэтому на том, что происходит сейчас, можно особо не зацикливаться.
- Существует is_faking_animation_frames где оказывается, что ребята из Gecko, в случае, если у вас много фейковых анимаций, выбрасывают вас на FakeRequestAnimationFrameCallback.
Даже не знаю, кто круче.
Хорошие ссылки:
- Background tabs & offscreen frames — Chrome team future plans
- requestAnimationFrame Scheduling For Nerds
- VSync if Firefox
- How to dramatically improve Chrome's requestAnimationFrame VSYNC accuracy in Windows
Автор: samsdemon