Главная / Статьи

Проектирование с учётом кэша «Назад/Вперед»: критерии подходящести, восстановление и состояние

Узнайте, как кэш «Назад/Вперед» браузера восстанавливает целые страницы, что молча этому препятствует, и как работать с восстановленным состоянием с помощью pageshow и pagehide.

Издатель: REACTAPP.TOP14 мин чтения3106 слов

Источники: The Browser Feature Most Frontend Developers Completely Ignore

Когда пользователь нажимает кнопку «Назад», большинство приложений тайно повторяют операции, которые браузер мог бы пропустить: снова загружают HTML, выполняют все скрипты, пересоздают DOM и загружают данные заново. Кэш для навигации вперед/назад (bfcache) существует именно для того, чтобы избежать этого, однако в повседневном коде его часто отключают незаметно для пользователя или неправильно обрабатывают страницу, которую он восстанавливает. В этом руководстве объясняется, чем bfcache отличается от обычного кэширования, какие привычки мешают его работе, как обнаруживать и реагировать на восстановленную страницу, а также как проверить её поведение на собственных устройствах.

Почему навигация по истории требует особого внимания

Работа над производительностью обычно сосредотачивается на первом посещении сайта. Команды сжимают изображения, делят бандлы, заранее загружают шрифты, сокращают время инициализации страницы, настраивают вызовы API и отслеживают показ самого крупного содержимого. Всё это полезно, но при этом подразумевается, что каждый просмотр страницы начинается с пустой вкладки и только что введённого URL-адреса.

Люди не просматривают контент именно так. Покупатель открывает товар, возвращается к списку результатов, открывает другой элемент, снова возвращается назад. Читатель переходит по ссылке из статьи и возвращается, чтобы дочитать её. Кто-то снова заходит на незаконченную форму. Кнопки «Назад» и «Вперед» относятся к самым часто используемым элементам управления в любом браузере, и такие действия являются обычными, а не редкими.

Браузеры оптимизируют этот процесс с помощью технологии bfcache. Вместо того чтобы полностью удалять страницу при уходе пользователя, браузер может сохранить копию всего документа в памяти. Когда пользователь возвращается через историю сессий, браузер может показать эту копию вместо того, чтобы повторять запросы к сети, парсинг HTML, выполнение скриптов, загрузку данных и отрисовку. Каждый крупный браузер реализует какую-то версию этой технологии, хотя точные правила применения и внутренние механизмы различаются у разных браузеров.

С точки зрения пользователя процесс успешного восстановления кажется практически мгновенным. Офсет прокрутки остается тем же, что и раньше, DOM не поврежден, поля формы могут сохранять свои значения, а куча JavaScript продолжает работать. Однако есть две проблемы: многие команды никогда не проверяют, соответствуют ли их страницы требованиям, а код, предполагающий, что каждый визит — это совершенно новая страница, может начать работать некорректно при возвращении старой страницы.

Поэтому bfcache — это не просто способ ускорения работы. Он позволяет учитывать более реалистичный цикл жизни страницы: она может находиться в спящем режиме, оставаясь активной, а затем снова запуститься без повторного выполнения кода загрузки.

В чем отличие bfcache от HTTP-кэша

Общее слово «кэш» вызывает первое недопонимание. Легко предположить, что bfcache означает просто повторное использование CSS, скриптов, изображений или ответов API, загруженных ранее. Это и есть HTTP-кэш, который работает на гораздо более мелком уровне.

Кэш HTTP хранит отдельные ответы. Возврат на страницу позволяет браузеру пропустить некоторые загрузки, но сам документ всё равно должен быть воссоздан: парсится HTML, выполняются скрипты, запускается фреймворк, создаётся DOM, восстанавливаются подписки и отправляются новые запросы.

bfcache сохраняет всю страницу, включая её DOM и кучу JavaScript. Когда пользователь уходит, выполнение скриптов может быть приостановлено; когда он возвращается через историю просмотра, браузер отображает сохранённую страницу и продолжает её обработку. Именно поэтому успешный запрос к bfcache превосходит даже тщательно настроенную перезагрузку, осуществляемую в основном из кэша HTTP: остаётся гораздо меньше работы.

Это меняет подход к рассуждениям о навигации. При восстановлении из bfcache обычный путь запуска не обязательно выполняется снова. Переменная на уровне модуля может по-прежнему хранить своё прежнее значение. DOM по-прежнему отражает то, что делал пользователь перед уходом. Таймеры и другая деятельность во время выполнения программы продолжают работу с того места, где остановились, вместо того чтобы создаваться заново.

Код, построенный на предположении, что возврат всегда означает полную перезагрузку, может упустить обновления, оставлять на экране устаревшее состояние или создавать дубликаты при попытке настроить то, что уже существует. Необходимая модель мышления проста: браузер не перерисовывает старый HTML-ответ; он просто возобновляет работу приостановленной страницы.

Большой успех, требующий практически никакого нового интерфейса

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

Ваша задача — не создавать собственную кэш-систему. Она заключается в том, чтобы не мешать браузеру использовать имеющийся у него кэш и сделать жизненный цикл вашей страницы совместимым с её восстановлением.

Это делает его одной из наиболее эффективных оптимизаций, доступных многим командам. Пágina, восстановленная из памяти, избегает повторных передач данных и большей части обработки, связанной с обычным навигированием. Преимущества наиболее заметны на медленных устройствах и сетях, но даже на быстрых компьютерах видна четкая разница между повторной генерацией страницы и её простым отображением.

Это также сохраняет весь опыт использования сайта, а не только момент времени. Представьте человека, который находится на середине длинной статьи, открывает соответствующую ссылку и затем возвращается обратно. При обычной перезагрузке необходимо воссоздать страницу, а затем попытаться восстановить положение прокрутки, часто после того, как макет изменился. Восстановление с помощью bfcache возвращает пользователя практически на то же место, где он находился ранее.

Списки товаров ещё яснее демонстрируют этот принцип. Человек использует четыре-пять фильтров, чтобы сузить каталог, просматривает длинный список, выбирает конкретный товар и затем возвращается назад. Если список сразу же появляется в том же состоянии, процесс кажется непрерывным. Если же происходит перезагрузка, повторный запрос данных, сброс фильтров или возврат к первой строке, сайт кажется нестабильным, независимо от того, насколько хороши показатели при первой загрузке.

Производительность — это не только скорость загрузки приложения. Это также степень, в которой продукт сокращает необходимость повторных действий при переходе между экранами.

Как привычка использовать метод unload тайно уничтожает его преимущества

Одной из причин того, что bfcache остается незамеченным, является то, что при отсутствии соответствия страницы определенным критериям ничего не ломается видимым образом. Браузер просто выполняет обычную навигацию по истории. Все продолжает работать; пользователи просто теряют возможность мгновенного восстановления состояния страницы.

Классическим виновником является событие unload. В старых версиях кода его использовали для отправки аналитики, освобождения ресурсов или уведомления сервера о закрытии страницы, исходя из предположения, что навигация всегда уничтожает страницу и дает последний шанс для очистки.

Эта теория больше не соответствует реальности. Страницу можно приостановить для использования в режиме bfcache вместо того, чтобы уничтожить её, а функция unload уже давно считается ненадёжной, особенно в мобильных устройствах, где она часто вообще не срабатывает. Поведение различается в зависимости от браузеров, но регистрация обработчика события unload может сделать страницу непригодной для использования в режиме bfcache в некоторых из них. Текущие рекомендации платформы настоятельно советуют воздерживаться от использования unload и предлагают события жизненного цикла, соответствующие современной модели. На момент написания этого текста Chrome также находится в процессе устаревания функции unload, поэтому для получения самой свежей информации следует обращаться к документации браузера.

Слушатель даже не обязан находиться в вашем собственном коде. Фрагменты аналитики, теги рекламы, всплывающие окна поддержки и более старые зависимости иногда регистрируют такие слушатели внутри самого приложения. Они проходят проверку кода, поскольку не оказывают видимого воздействия, однако могут удалять оптимизации на уровне браузера при каждом посещении этой страницы.

Замена метода unload не сводится к простому переименованию события. Подходящая замена зависит от того, для чего на самом деле предназначен код:

  • Данные аналитики можно отправлять с помощью метода navigator.sendBeacon() или соответствующим образом настроенного запроса fetch (например, с параметром keepalive) при изменении видимости страницы на скрытую.
  • Предупреждения о несохраненных данных формы можно отображать с использованием метода beforeunload, но такой слушатель следует добавлять только тогда, когда есть несохраненные изменения, и удалять его после их сохранения, а не оставлять его активным на протяжении всего срока существования страницы.
  • Ресурсы, связанные с активной страницей, можно приостановить и возобновить с помощью событий жизненного цикла, совместимых с bfcache, о которых будет рассказано далее.
  • Общий урок: переход на другую страницу не означает окончания жизненного цикла текущей страницы. Код, основанный на этом принципе, оказывается менее надежным и работает медленнее, чем следует.

    Обнаружение восстановления с помощью pageshow и pagehide

    Два события охватывают скрытый период. Событие pagehide срабатывает, когда навигация уводит документ за пределы экрана, а событие pageshow — когда навигация снова делает документ активным, включая восстановление из bfcache. Оба события имеют свойство persisted. При событии pageshow значение persisted равно true, когда страница вышла из bfcache; при событии pagehide оно указывает на попытку браузера сохранить страницу. В отличие от события unload, событие pagehide разработано для совместного использования с bfcache.

    Приведённый ниже фрагмент демонстрирует базовую структуру. Обработчик pageshow возвращает результат рано при обычной загрузке, поэтому логика обновления запускается только тогда, когда возвращается сохранённая страница. Обработчик pagehide приостанавливает работы в реальном времени каждый раз, когда страница скрывается, независимо от того, сохраняется ли она в кэше. Вспомогательные функции предназначены как место для вашей собственной логики приложения.

    window.addEventListener("pageshow", (event) => {
      if (!event.persisted) {
        return;
      }
    
    refreshSessionState();
      refreshTimeSensitiveData();
    });
    
    window.addEventListener("pagehide", () => {
      pauseRealtimeConnections();
    });
    

    Обратите внимание, чего здесь не происходит: не выполняется повторная загрузка всего контента. Перезапуск всего слоя данных при каждом восстановлении лишил бы большей части преимуществ. Цель — обновлять только ту информацию, корректность которой может существенно измениться во время приостановки работы страницы.

    Что именно это означает, зависит от типа страницы:

    • На главной странице новостей может потребоваться поиск свежих заголовков.
    • На экране учётной записи может понадобиться подтверждение того, что сессия не была завершена в другой вкладке.
  • На этапе оформления заказа может потребоваться повторная проверка наличия товаров и цен.
  • На странице с статической документацией обычно можно полностью обойтись без логики восстановления.
  • Настоящее улучшение заключается в том, что теперь приложение может отличать новую загрузку от восстановления. Без такого различия команды обычно вынуждены выбирать между двумя плохими вариантами: никогда не обновлять контент и рисковать устаревшей информацией, или перезапускать полную инициализацию и терять большую часть скорости. Приложение, понимающее механизм bfcache, рассматривает восстановление как отдельный этап жизненного цикла, сохраняя то, что всё ещё действительно актуально, и обновляя то, чему больше нельзя доверять.

    Когда восстановленное состояние становится проблемой корректности

    Мгновенное восстановление кажется чисто положительным решением, пока на сохранённой странице не окажется чего-то, что уже не должно отображаться.

    Рассмотрим пользователя, который открывает панель управления своим аккаунтом, переходит на другую страницу, выходит из второй вкладки и в конце возвращается назад. Восстановленный экран может по-прежнему отображать данные предыдущей сессии. Браузер не нарушает никаких правил безопасности, запоминая страницу в рамках одной сессии просмотра, но вашему приложению может потребоваться проверить, действительно ли сессия активна, прежде чем разрешить дальнейшее взаимодействие.

    Аналогичная проблема возникает с данными, чувствительными к времени. Билет мог быть куплен в другой вкладке, запись могла быть изменена коллегой, или окно доступности может уже закончиться. Восстановленная страница может быть идеальным отражением прошлого, но при этом оставаться неверным источником информации в настоящем.

    Распространенной реакцией является полная отключка кэширования. Обычно это слишком радикальный подход. Страница может сохранить свое состояние DOM и информацию об взаимодействии, обновляя только те данные, которые должны быть актуальными, причем pageshow — это подходящее место для такой синхронизации. В официальных рекомендациях четко говорится, что страницы с часто меняющимся содержимым могут загружать свежие данные после восстановления, вместо того чтобы полностью отказываться от bfcache.

    Страницы, критичные с точки зрения безопасности, по-прежнему требуют тщательно продуманных правил кэширования и сессий, а способ обработки браузерами заголовков вроде Cache-Control: no-store постоянно меняется. Например, Chrome начал разрешать определенным страницам с настройкой no-store попадать в bfcache при соблюдении определенных условий. Избегайте использования упрощенных предположений относительно того, как каждый браузер обрабатывает такой заголовок, и всегда проверяйте актуальную документацию браузера перед тем, как на нее опираться.

    Более безопасный подход — четко определить:

    • какое состояние можно безопасно сохранить в текущем виде;
    • какое состояние необходимо повторно проверить после восстановления;
    • какие действия пользователя всегда требуют новой проверки авторизации на сервере, независимо от того, что показывает интерфейс в данный момент.

    Последняя категория имеет наибольшее значение. Клиент может обновить свой отображаемый вид, но сервер никогда не должен доверять восстановленной странице как доказательству того, что пользователь все еще авторизован.

    Паузирование долгоживущих соединений во время приостановки

    Открытые соединения, такие как WebSockets, объекты IndexedDB, общие блокировки и запросы в процессе выполнения, усложняют работу буфера bfcache, поскольку браузер вынужден замораживать страницу, в то время как другие вкладки и документы продолжают работать. Замороженная страница, удерживающая общую блокировку или открытое соединение с базой данных, может мешать страницам, которые все еще находятся в активном состоянии. Правила отбора для хранения в кэше различаются в зависимости от браузера, и в некоторых случаях достаточно наличия открытого или активного соединения, чтобы страница не попала в кэш. Рекомендуемый подход — освобождать или приостанавливать использование таких ресурсов при скрытии страницы, а затем снова создавать их с осторожностью, как только она становится видимой.

    Это не означает необходимости удалять все обработчики событий или наблюдатели. Те элементы, которые находятся полностью внутри страницы, такие как обработчики DOM, обычно могут оставаться активными без вреда. Следует сосредоточиться на тех компонентах, которые взаимодействуют с внешними системами за пределами замороженной документации, или на тех элементах, которые не должны казаться активными, когда никто не просматривает страницу.

    Например, панель управления в реальном времени может закрыть свой WebSocket при событии pagehide и открыть новый, когда страница восстановлена. Реализация должна защищать от дублирования соединений, поскольку как обычный путь запуска, так и обработчик восстановления могут попытаться установить соединение; простой способ сделать её идемпотентной — использование одной функции connect(), которая проверяет наличие уже существующего соединения. Обертка для IndexedDB может закрыть свое соединение с базой данных перед приостановкой работы и снова открыть его по мере необходимости.

    Эта практика приносит пользу не только в контексте bfcache. Она заставляет команду определять, кто отвечает за каждый внешний ресурс, когда он должен существовать и что происходит, когда документ становится неактивным. Такие правила одинаково важны как для фоновых вкладок, так и для мобильных устройств, которые замораживают страницы, а также при прерванных сессиях.

    Приложения с одной страницей по-прежнему получают преимущества

    bfcache работает с навигацией между документами, которыми управляет браузер. Изменения маршрутов, обрабатываемые с помощью JavaScript без загрузки нового документа, остаются вне его области применения. Переход с /products на /products/42 через клиентского маршрутизатора по-прежнему является задачей, которую решают само приложение с одной страницей и его фреймворк.

    Тем не менее, у SPA есть много преимуществ. Пользователи часто покидают приложение, чтобы перейти на отдельную страницу, сайт стороннего разработчика, сервис входа или совершенно другое приложение, а затем возвращаются обратно. В таком случае браузер может восстановить всё приложение целиком, вместо того чтобы снова загружать пакеты, перерисовывать каждый компонент и повторно выполнять процедуру настройки. Официальные рекомендации прямо указывают на это преимущество, отмечая при этом, что изменения маршрутов внутри приложения находятся вне контроля bfcache.

    Такие возвраты могут выявить предположения на уровне фреймворка:

    • ожидание того, что инициализация корневого узла будет выполняться каждый раз, когда документ становится видимым;
    • наличие слоя данных, предполагающего, что его кэши всегда пусты;
    • создание нового соединения в реальном времени одновременно с возобновлением старого;
    • отображение компонентами данных, которые были корректны в момент ухода и теперь могут быть устаревшими.

    Ни одна фреймворк-система не скрывает собственный жизненный цикл браузера. Независимо от того, используете ли вы React, Vue, Angular или Svelte, код всё равно выполняется внутри документа, который браузер может сохранить и восстановить. Фреймворки хорошо справляются с организацией состояния, но у них нет способа самостоятельно определить, какие бизнес-данные остаются действительными после приостановки работы. Например, в React компоненты не перезагружаются при восстановлении из bfcache, поэтому эффекты, запущенные при первой загрузке, не будут выполняться снова; для того чтобы приложение поняло, что прошло время, используется обработчик pageshow.

    Многие разработчики фронтенда гораздо лучше знают поведение механизмов загрузки и разгрузки компонентов в своих фреймворках, чем жизненный цикл документа браузера. bfcache показывает, что процессы загрузки и разгрузки описывают лишь один из аспектов того, что на самом деле происходит с страницей.

    Тестирование bfcache за несколько минут

    bfcache остаётся игнорируемым отчасти потому, что сложно определить, работает ли он. Возврат на страницу может казаться быстрым, а панель Сеть мало помогает, поскольку восстановленная страница полностью пропускает обычную последовательность запросов.

    В Chrome DevTools предусмотрена специальная проверка bfcache, доступная в панели Приложения. После её запуска Chrome переходит на страницу и возвращается обратно, после чего отображается информация о том, была ли страница восстановлена. Если страница не соответствует критериям, DevTools указывает конкретные причины и определяет, какие фреймы в этом виноваты.

    Включите эту проверку в регулярный контроль производительности для важных страниц, особенно результатов поиска, статей, каталогов товаров, панелей управления и любых экранов, которые пользователи часто посещают. Также выполняйте её с включёнными скриптами от сторонних разработчиков, поскольку соответствие критериям часто блокируется кодом, находящимся вне вашего собственного пакета.

    Что касается данных с фронта, информация notRestoredReasons, предоставляемая через API производительности (в разделе с данными о времени навигации), может объяснить, почему реальные истории навигации не были восстановлены. Поддержка этой функции не является универсальной, поэтому следует рассматривать её как инструмент постепенного внедрения, а не как что-то, от чего зависит ваш код во всех браузерах. Она всё равно полезна, поскольку ни один локальный тест не может воссоздать все расширения браузера, сторонние iframe, открытые соединения, заголовки или условия работы браузера, с которыми приходят ваши пользователи. Более лёгкой альтернативой является запись значения event.persisted из события pageshow в систему аналитики для измерения фактической степени попадания запросов.

    Не стоит ожидать, что каждая навигация по истории будет сохраняться в кэше. Браузеры удаляют кэшированные копии для оптимизации использования памяти, а некоторые факторы, мешающие сохранению кэша, находятся вне контроля страницы. Реалистичная цель — устранять те факторы, которые можно избежать, и правильно обрабатывать восстановление кэша каждый раз, когда это происходит.

    Заключение

    bfcache легко упускать из виду именно потому, что ничто в вашем коде не активирует его. Не существует никаких пакетов, компонентов или файлов конфигурации, которые делали бы его частью архитектуры; браузер просто пытается обеспечить пользователям лучший опыт. Эта невидимость имеет двойственный эффект: когда он работает, пользователи получают практически мгновенную навигацию по истории практически без дополнительного кода; когда он блокируется, сайт замедляет работу без появления ошибок; а при неправильном восстановлении кэша снова появляется устаревшее состояние, поскольку приложение предполагает, что возврат к прошлому всегда означает перезагрузку.

    Практический чек-лист для самых часто посещаемых страниц:

    • Удалите обработчики события unload, включая те, что добавлены скриптами сторонних разработчиков, и перенесите их функции в pagehide, visibilitychange или sendBeacon().
    • Привяжите обработчик beforeunload только тогда, когда есть несохраненные изменения.
    • Закройте или приостановите внешние соединения в момент pagehide, а при восстановлении страницы снова подключитесь к ним.
    • Используйте pageshow вместе с параметром event.persisted, чтобы обновлять только те данные, которые могут устареть, и сохраняйте проверки авторизации на сервере.
    • Выполните тест кэша в DevTools и отслеживайте реальные показатели восстановления страниц там, где это возможно согласно API.

    Основное изменение касается того, как мы представляем себе страницу. Вместо одной прямой линии от создания до уничтожения её жизненный цикл может включать настройку, скрытие, заморозку, восстановление, согласование и дальнейшую работу. Команды вкладывают много усилий в ускорение процесса реконструкции страницы, тогда как браузер часто может полностью пропустить этот процесс и вернуть пользователям ту же страницу, которая у них уже была.