BFcache — технология оптимизации работы браузера, обеспечивающая мгновенную отдачу ранее просмотренной страницы при помощи кнопок «Вперед» и «Назад». Этот паттерн значительно улучшает пользовательский опыт, особенно у пользователей, обладающих слабенькими устройствами или просматривающих сайт из-под медленных сетей. Люди пользуются кнопкой возврата, возможно, даже чаще, чем вы думаете. А если так, то зачем сразу выбрасывать страницу из памяти браузера, а спустя мгновение тратить трафик на её повторное открытие?
Веб-разработчикам необходимо понимать, как улучшить взаимодействие пользователей с их продуктом. Давайте посмотрим, каким образом для решения этой задачи может быть использован BFcache.
Поддержка браузерами
BFcache уже много лет поддерживается как в Firefox, так и в Safari на всех устройствах.
Начиная с версии 86, Chrome поддерживает BFcache для межсайтовой навигации на Android для небольшого процента пользователей. В Chrome 87 поддержка BFcache будет развернута для всех пользователей Android при работе с межсайтовой навигацией, с планами поддерживать навигацию на том же сайте в ближайшем будущем.
Основы BFcache
Сколько раз вам приходилось переходить по ссылке на сайте, чтобы, уйдя на другую страницу, понять, контент вам не интересен, и возвращаться при помощи кнопки «Назад»? В этот момент BFcache может существенно ускорить отдачу предыдущей страницы.
BFcache — это in-memory кэш, в котором хранится снепшот страницы (включая JS-heap), после того, как пользователь покидает её URL. Когда вся страница находится в памяти, браузер может быстро и легко восстановить ее, если пользователь решит вернуться.
Без поддержки BFcache при возвращении на предыдущую страницу инициируется новый запрос к ней. Таким образом, в зависимости от того, насколько страница оптимизирована под повторную загрузку, браузеру придется заново запросить некоторые (или все) ресурсы, потратить время и мощности на их повторный анализ и исполнение.
BFcache не только ускоряет навигацию по сайту, но и позволяет бережнее расходовать трафик в силу того, что не нужно повторно загружать ресурсы.
Данные об использовании Chrome показывают, что каждый десятый переход с десктопного устройства и каждый пятый с мобильного выполняется либо назад, либо вперед. С включенным BFcache браузеры могут исключить передачу данных и сэкономить время, затрачиваемое на загрузку миллиардов веб-страниц каждый день!
Как это работает?
Кэширование, используемое BFcache, отличается от HTTP-кэша. BFcache — это снепшот всей страницы в памяти (включая JS-heap), тогда как HTTP-кэш содержит только ответы на ранее сделанные запросы. Поскольку все запросы, необходимые для загрузки страницы, довольно редко могут быть выполнены из HTTP-кэша, повторные посещения с восстановлением из BFcache всегда быстрее, чем даже самые оптимизированные переходы без использования этой опции.
Тем не менее, хранение снепшота страницы в памяти связано с некоторыми сложностями с точки зрения того, как лучше всего сохранить текущий код. Например, как обработать вызовы
setTimeout()
, когда истекает время ожидания, если страница находится в BFcache?Ответ заключается в том, что браузеры приостанавливают выполнение любых ожидающих таймеров или неразрешенных промисов, и вообще всех отложенных задач, и возобновляют их обработку, когда (или если) страница восстанавливается из BFcache.
В некоторых случаях риски, что «всё пойдет не так», весьма малы, особенно когда мы имеем дело с таймерами или промисами. В других же случаях пользователь с высокой долей вероятности может столкнуться с неожиданным поведением страницы. Например, если браузер приостанавливает выполнение задачи, которая требуется как часть транзакции IndexedDB, это может повлиять на другие открытые вкладки в том же домене (поскольку к одним и тем же базам данных IndexedDB можно получить доступ с нескольких вкладок одновременно). В результате браузеры обычно не будут пытаться кэшировать страницы в середине транзакции IndexedDB, или использовать API, которые могут повлиять на другие страницы.
API для работы с BFcache
Несмотря на то, что BFcache — это оптимизация, выполняемая браузерами автоматически, разработчикам важно знать, когда это происходит, чтобы они могли оптимизировать свои страницы для этого и соответствующим образом скорректировать любые показатели при измерения производительности. Основными событиями, используемыми для работы BFcache, являются события перехода страницы: pageshow и pagehide. Они поддерживаются почти во всех современных браузерах. Новые события жизненного цикла страницы, freeze и resume, также отправляются, когда страницы записываются или считываются из BFcache, а также в некоторых других ситуациях. Например, когда фоновая вкладка замораживается для минимизации использования ЦП.
Обратите внимание, что события freeze и resume в настоящее время поддерживаются только в браузерах на основе Chromium.
Восстановление страницы из BFcache
Событие pageshow срабатывает сразу после события загрузки, когда страница загружается изначально, и каждый раз, когда страница восстанавливается из BFcache. У события pageshow есть свойство persisted, которое будет истинным, если страница была восстановлена из BFcache (и false, если нет). Вы можете использовать свойство persisted, чтобы отличить обычную загрузку страницы от восстановления из BFcache. Например:
window.addEventListener ('pageshow', function (event) {
if (event.persisted) {
console.log ('Эта страница восстановлена из BFcache.');
} else {
console.log ('Эта страница загрузилась по запросу.');
}
});
В браузерах, поддерживающих Page Lifecycle API, событие resume также срабатывает при восстановлении страниц из BFcache (непосредственно перед событием pageshow). Хотя оно также запускается, когда пользователь повторно посещает замороженную фоновую вкладку. Если вы хотите восстановить состояние страницы после ее замораживания (включая страницы в BFcache), то можете использовать событие возобновления. Но если вы хотите измерить процент попаданий в BFcache вашего сайта, вам нужно будет использовать событие pageshow. В некоторых случаях вам может понадобиться использовать оба события.
Запись страницы в BFcache
Событие pagehide является аналогом события pageshow. Событие pageshow запускается, когда страница либо загружается нормально, либо восстанавливается из BFcache. Событие pagehide возникает, когда страница либо выгружается в обычном режиме, либо когда браузер пытается поместить ее в BFcache.
Событие pagehide также имеет свойство persisted, и если оно false, то можете быть уверены, что страница не собирается записаться в BFcache. Однако, если сохраненное свойство истинно, это не гарантирует, что страница будет кэширована. Это означает, что браузер намеревается кэшировать страницу, но могут иметь место факторы, делающую запись невозможной.
window.addEventListener('pagehide', function(event) {
if (event.persisted === true) {
console.log('*Возможно*, страница записана в BFcache.');
} else {
console.log('Эта страница будет выгружена нормально и будет удалена из памяти.');
}
});
Как и в предыдущем примере для браузеров с поддержкой Page Lifecycle API, событие freeze срабатывает сразу после события pagehide (если свойство persisted события истинно). Но, опять же, это означает, что браузер намерен закешировать страницу. Возможно, кэширование не произойдет.
Помните про оптимизацию страниц
Не все страницы попадают в BFcache. Те, что действительно закешировались, не пребывают там бесконечно. Очень важно, чтобы разработчики понимали, что делает страницы подходящими (или неприемлемыми) для BFcache, чтобы максимизировать количество их попаданий в кэш. Ниже рассмотрим методы, повышающие вероятность кэширования страниц браузером.
Никогда не используйте событие unload
Первый шаг при оптимизации страниц для BFcache — отказ от использования события unload.
До недавнего времени разумно предполагалось, что страница прекращает свое существование в памяти после события unload, т. е. сразу после того, как пользователь покинул страницу. Реалии, продиктованные временем, изменились. Всякий раз, когда посетитель покидает страницу, браузер сталкивается с дилеммой: улучшение пользовательского опыта путем кэширования страницы против вероятности показа пользователю неактуальных данных.
FireFox не добавляет страницу в BFcache, если в ней используется событие unload. Safari же пытается кэшировать такие страницы, но оперирует этим событием только когда пользователь действительно покидает страницу. Chrome при событии unload ведет себя примерно как Safari.
В качестве альтернативы используйте событие pagehide. Оно запускается во всех случаях, когда в данный момент запускается событие unload, а также возникает, когда страница помещается в BFcache.
Lighthouse v6.2.0 добавил в аудит пункт no-unload-listeners, предупреждающий разработчиков, если какой-либо JavaScript на их страницах (в том числе из сторонних библиотек) добавляет прослушивание события unload.
Иными словами, если вы хотите повысить скорость работы вашего сайта в FireFox, Safari и Chrome, перестаньте слушать событие unload. Вместо него слушайте событие pagehide.
С осторожностью используйте событие beforeunload
Событие beforeunload не воспрепятствует попаданию страницы в BFcache в Chrome и Safari, зато воспрепятствует в случае с FireFox. Используйте это событие только при необходимости.
Однако у прослушивания этого события есть вполне конкретные случаи применения. Например, предупредить не сохранившего форму пользователя о том, что он будет вынужден заполнить форму снова, если покинет страницу. В этом примере рекомендуется слушать событие только при заполнении пользователем данных.
Антипаттерн. Событие прослушивается безусловно:
window.addEventListener ('beforeunload', (событие) => {
if (pageHasUnsavedChanges ()) {
event.preventDefault ();
return event.returnValue = 'Вы уверены, что хотите выйти?';
}
});
Паттерн. Событие прослушивается только при необходимости:
unction beforeUnloadListener (событие) {
event.preventDefault ();
return event.returnValue = 'Вы уверены, что хотите выйти?';
};
// Функция, которая запускает коллбек, когда на странице есть несохраненные изменения.
onPageHasUnsavedChanges (() => {
window.addEventListener ('beforeunload', beforeUnloadListener);
});
// Функция, которая запускает коллбек, когда несохраненные изменения страницы разрешены.
onAllChangesSaved (() => {
window.removeEventListener ('beforeunload', beforeUnloadListener);
});
Избегайте использования window.opener
В некоторых браузерах (включая Chrome версии 86), если страница была открыта с помощью
window.open ()
или по ссылке с target="_blank"
без указания rel="noopener"
, открывающая страница будет содержать ссылку на оконный объект открытой страницы. Такие страницы не попадут в BFcache, т. к. существует угроза безопасности и могут сломаться другие страницы, пытающиеся получить к ним доступ.Закрывайте соединения перед уходом пользователя со страницы
Как упоминалось выше, при записи страницы в BFcache все запланированные JS-задачи приостанавливаются, а затем возобновляются, когда страница считывается оттуда. Если эти задачи обращаются только к API, изолированным лишь для текущей страницы (например, DOM), то приостановка выполнения этих задач, пока страница не видна пользователю, не вызовет никаких проблем. Но если имеют место задачи, связанные с API-интерфейсами, которые также доступны с других страниц того же домена (например, IndexedDB, Web Locks, WebSockets и т. д.), можно столкнуться с проблемой, поскольку приостановка этих задач может помешать запуску кода на других вкладках.
Иными словами, браузер даже не будет пытаться записать в BFcache страницы, где:
- не завершена транзакция к IndexDB;
- выполняется
fetch()
илиXMLHttpRequest;
- открыто соединение по WebSocket или WebRTC.
Если что-то из приведенного выше списка актуально для страницы, приостановите работу с ними во время прослушивания событий pagehide или freeze, что повысит шанс записать страницу в BFcache. Вы всегда сможете возобновить работу с этими API при считывании страницы из кэша.
Тесты вам в помощь
Хоть и нет способа определить, была ли страница помещена в кэш, можно удостовериться, что переход назад или вперед восстановил страницу из кэша.
В настоящее время в Chrome страница может оставаться в BFcache до трех минут, что должно быть достаточно для запуска теста (с использованием такого инструмента, как Puppeteer или WebDriver), чтобы убедиться, что свойство события pageshow истинно, а затем нажмите кнопку назад.
Обратите внимание, что, хотя в нормальных условиях страница должна оставаться в кэше достаточно долго, чтобы запустить тест, она может в любой момент оттуда исчезнуть (например, если система испытывает нехватку памяти). Неудачный тест не обязательно означает, что ваши страницы не кэшируются, поэтому вам необходимо соответствующим образом настроить критерии отказа теста или сборки.
В Chrome в настоящее время BFcache включен только на мобильных устройствах. Чтобы протестировать BFcache на десктопе, вам необходимо включить флаг
# back-forward-cache
.Отключение BFcache
Если вы не хотите, чтобы страница сохранялась в BFcache, вы можете убедиться, что она не кэшируется, установив заголовок:
Cache-Control: no-store
Все другие директивы кэширования (включая отсутствие кэширования или даже отсутствие хранения в зависимом фрейме) не влияют на право страницы на использование BFcache.
Хотя этот метод эффективен и работает во всех браузерах, он имеет другие последствия для кэширования и производительности, которые могут быть нежелательными. Для решения этой проблемы предлагается добавить более явный механизм отказа, в том числе механизм очистки BFcache, если это необходимо (например, когда пользователь выходит из веб-сайта на общем устройстве).
Кроме того, в Chrome в настоящее время возможен отказ на уровне пользователя с помощью флага
# back-forward-cache
, а также отказ на основе корпоративной политики.Внимание: учитывая улучшениt пользовательского опыта средствами BFcache, не рекомендуется его отключать, кроме случаев необходимости по соображениям конфиденциальности, например, если пользователь выходит из веб-сайта на общем устройстве.
Влияние на аналитику
Если вы отслеживаете посещения своего сайта с помощью инструмента аналитики, то, вероятно, заметите уменьшение общего количества просмотров страниц, поскольку Chrome продолжает включать BFcache для большего числа пользователей.
Фактически, вы, вероятно, уже занижаете количество просмотров страниц из других браузеров, которые реализуют BFcache, поскольку большинство популярных аналитических библиотек не отслеживают восстановление BFcache как новые просмотры страниц.
Если вы не хотите, чтобы количество просмотров страниц уменьшалось из-за включения BFcache, можете сообщить о восстановлении BFcache как о просмотрах страниц (рекомендуется), прослушав событие pageshow и проверив свойство persisted.
В следующем примере показано, как это сделать с помощью Google Analytics (или любых аналогичных инструментов):
// Отправляем просмотр страницы при первой загрузке страницы.
gtag ('event', 'page_view')
window.addEventListener ('pageshow', function (event) {
if (event.persisted === true) {
// Отправить еще один просмотр страницы, если страница восстановлена из bfcache.
gtag ('event', 'page_view')
}
});
Измерения производительности
BFcache также может отрицательно повлиять на показатели производительности, собираемые в полевых условиях (в частности, время загрузки страницы).
Поскольку навигация BFcache считывает существующую страницу, а не инициирует загрузку новой страницы, общее количество собранных загрузок страниц будет уменьшаться при включении BFcache. Критично то, что загрузка страниц, замененная считыванием из BFcache, вероятно, была бы одной из самых быстрых загрузок страниц в вашем наборе данных. Это связано с тем, что переходы вперед и назад по определению являются повторными посещениями, а повторные загрузки страниц обычно выполняются быстрее, чем загрузка страниц при первом посещении (из-за кэширования HTTP, как упоминалось ранее). В результате в вашем наборе данных будет меньше быстрых загрузок страниц, что, вероятно, ухудшит показатели, несмотря на то, что производительность, с которой сталкивается пользователь, скорее всего улучшится!
Есть несколько способов справиться с этой проблемой. Один из них — аннотировать все показатели загрузки страницы соответствующим типом навигации: навигация, перезагрузка, back_forward или предварительная визуализация. Это позволит вам продолжать следить за своей производительностью в рамках этих типов навигации. Такой подход рекомендуется для показателей загрузки страницы, не ориентированных на пользователя, таких как время до первого байта (TTFB). Для показателей, ориентированных на пользователя, таких как Core Web Vitals, лучший вариант — сообщить значение, которое точнее отражает то, что испытывает пользователь.
Внимание: тип навигации back_forward в Navigation Timing API не следует путать с восстановлением BFcache. Navigation Timing API только аннотирует загрузку страниц, тогда как считывание из BFcache повторно использует страницу, загруженную из предыдущей навигации.
Влияние на Core Web Vitals
Core Web Vitals измеряет взаимодействие пользователя с веб-страницей по различным параметрам (скорость загрузки, интерактивность, визуальная стабильность), и, поскольку пользователи воспринимают считывание из BFcache как более быстрое перемещение по сравнению с традиционной загрузкой страниц, важно, чтобы показатели Core Web Vitals отражали это. В конце концов, пользователя не волнует, включен BFcache или нет, его просто заботит, чтобы навигация была быстрой!
Такие инструменты, как отчет о пользовательском опыте в Chrome, которые собирают и предоставляют отчеты о показателях Core Web Vitals, скоро будут обновлены, чтобы обрабатывать считывание BFcache как отдельные посещения страницы в наборе данных.
И хотя (пока что) не существует специализированных API веб-производительности для измерения этих показателей после восстановления BFcache, их значения можно приблизительно оценить с помощью существующих веб-API.
- Для Largest Contentful Paint (LCP) вы можете использовать дельту между отметкой времени события pageshow и отметкой времени следующего нарисованного кадра (поскольку все элементы в кадре будут нарисованы одновременно). Обратите внимание, что в случае восстановления bfcache LCP и FCP будут одинаковыми.
- Для First Input Delay (FID) вы можете повторно добавить подписчики на события (те же, что используются полифилом FID) в событии pageshow и сообщить FID как задержку первого ввода после считывания из BFcache.
- Для Cumulative Layout Shift (CLS) вы можете продолжать использовать существующий Performance Observer. Всё, что вам нужно сделать, это добиться текущего значения CLS, близкого (а лучше равного) 0.
Для получения дополнительной информации о том, как BFcache влияет на каждую метрику, обратитесь к отдельным страницам руководств по метрикам Core Web Vitals. А конкретный пример того, как реализовать версии этих показателей для BFcache в коде, можно найти в PR, добавив их в библиотеку JS web-vitals.
Начиная с версии 1, библиотека JavaScript web-vitals поддерживает восстановление BFcache в отчетных показателях. Разработчикам, использующим v1 или выше, не нужно обновлять свой код.