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

Что на самом деле делает bfcache

Обычная загрузка страницы — процесс дорогостоящий. Браузер должен загрузить ресурсы, токенизировать HTML, построить DOM, запустить скрипты, разрешить стили, выполнить компоновку (layout), отрисовать пиксели и скомпоновать слои. bfcache обходит почти всё это, сохраняя страницу в замороженном состоянии в оперативной памяти (RAM). Это не дисковый кэш. Отрисованная страница, включая кучу JavaScript, позицию прокрутки и состояние форм, находится в памяти, пока пользователь читает следующую страницу. Когда пользователь нажимает «Назад», браузер «размораживает» снимок и вызывает событие pageshow. Страница возобновляет работу, не обращаясь к сети и не выполняя компоновку заново. Для пользователей на медленных устройствах или при нестабильном соединении разница между восстановлением из bfcache и новой загрузкой может составлять сотни миллисекунд и более.

Что ломает bfcache

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

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

Две страницы не сработали. Страница с обработчиком события unload не смогла восстановиться. Страница с открытым WebSocket-соединением также была заблокирована. Эти два сбоя указывают на ловушки, в которые ежедневно попадают реальные рабочие сайты.

Ловушка события unload

Событие unload долгое время было основным сигналом для выполнения очистки в последнюю секунду. Разработчики используют его для отправки аналитических маяков (beacons), остановки таймеров или очистки временного состояния. Проблема в том, что bfcache построен на идее, что страница может «ожить». Если браузер видит слушатель unload, он предполагает, что страница ожидает полного уничтожения, и отказывается её замораживать. Неважно, пуста ли прикрепленная функция. Само наличие слушателя достаточно, чтобы наложить вето на кэширование во всех современных браузерах.

Заменой является pagehide. Это событие срабатывает как при заморозке страницы для bfcache, так и при её реальном удалении. Если вам нужно различить эти два случая, свойство event.persisted будет равно true, когда страница переходит в bfcache. Однако для большинства задач по завершению работы pagehide покрывает оба пути. Перенесите всю логику очистки из unload в pagehide. Затем полностью удалите все слушатели unload, включая те, что спрятаны в сторонних аналитических сниппетах или устаревших плагинах.

Ловушки активных соединений

Открытое сетевое или системное соединение сигнализирует о том, что ваша страница всё еще выполняет реальную работу. В момент навигации браузер проводит инвентаризацию активных ресурсов. Если он обнаруживает открытый WebSocket, активное WebRTC peer connection или незавершенное соединение IndexedDB, он прерывает заморозку и уничтожает страницу обычным способом. Снимку нельзя доверять, пока могут продолжать передаваться байты.

Вам следует закрывать эти ресурсы внутри слушателя pagehide. Вызывайте метод close() вашего WebSocket. Закрывайте WebRTC peer connections. Прерывайте или завершайте любые незавершенные транзакции IndexedDB. Если вашему приложению нужны эти каналы при возвращении пользователя, открывайте их внутри pageshow. Этот паттерн «закрытие при pagehide, восстановление при pageshow» позволяет странице оставаться подходящей для мгновенной навигации назад без потери функциональности.

Сюрприз с no-store

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