Проєктування для кешу «назад/вперед»: умови використання, відновлення та стан
Дізнайтеся, як буфер «Назад/Вперед» браузера відновлює цілі сторінки, що тихо заважає цьому процесу, та як працювати з відновленим станом за допомогою pageshow та pagehide.
Джерела: The Browser Feature Most Frontend Developers Completely Ignore
Коли користувач натискає «Назад», більшість додатків тихо повторюють роботу, яку браузер міг би пропустити: знову завантажують HTML, виконують усі скрипти, перебудовують DOM та завантажують дані знову. Буфер для навігації «Назад/Вперед» (bfcache) існує саме для того, щоб уникнути цього, проте щоденний код часто вимикає його без усвідомлення користувача або неправильно обробляє сторінку, яку він повертає. У цьому посібнику пояснюється, чим bfcache відрізняється від звичайного кешування, які звички його блокують, як виявити та відреагувати на відновлену сторінку та як перевірити його роботу на власних екранах.
Чому навігація за історією відвідувань потребує окремої уваги
Робота над продуктивністю зазвичай зосереджується на першому відвідуванні. Команди стискають зображення, ділять пакети дань, завантажують шрифти заздалегідь, скорочують витрати на ініціалізацію, налаштовують виклики API та стежать за показником Largest Contentful Paint. Усе це корисно, але при цьому мовчки передбачається, що кожне переглядання сторінки починається з порожньої вкладки та нововведеного URL-адреси.
Люди не переглядають вміст саме так. Покупець відкриває товар, повертається до списку результатів, відкриває інший товар та знову повертається назад. Читач переходить за посиланням у статті та повертається, щоб її дочитати. Хтось знову переглядає неповністю заповнену форму. Кнопки «Назад» та «Вперед» належать до найчастіше використовуваних елементів керування в будь-якому браузері, і такі дії є звичайними, а не винятковими.
Браузери оптимізують цю схему за допомогою bfcache. Замість того, щоб видаляти сторінку після того, як користувач її залишає, браузер може зберігати копію всього документа в пам’яті. Коли користувач повертається через історію сеансів, браузер може відобразити цю копію, замість того щоб повторювати запит до мережі, обробку HTML, виконання скриптів, завантаження даних та їх відображення. Кожен основний движок реалізує якусь версію цього механізму, хоча точні правила застосування та внутрішні принципи роботи відрізняються між браузерами.
З боку користувача процес успішного відновлення здається майже миттєвим. Офсет прокрутки залишається таким, яким він був, DOM залишається недоторканим, поля форми можуть все ще містити свої значення, а кеш JavaScript просто продовжує функціонувати. Проблема полягає у двох аспектах: багато команд ніколи не перевіряють, чи підходять їхні сторінки для відновлення, а код, який припускає, що кожен візит — це абсолютно нова сторінка, може поводитися некоректно, коли повертається стара.
Отже, bfcache — це не просто безкоштовне прискорення. Він дозволяє бачити більш реалістичний цикл життя сторінки: вона може залишатися «сплячою», залишаючись при цьому активною, а потім прокинутися пізніше без необхідності повторного виконання коду запуску.
У чому різниця між bfcache та HTTP-кешем
Спільне слово «cache» спричиняє першу плутанину. Легко припустити, що bfcache означає просте повторне використання CSS, скриптів, зображень чи відповідей API, які були завантажені раніше. Це і є HTTP-кеш, який працює на набагато більш дрібному рівні.
Кеш HTTP зберігає окремі відповіді. Повернення на сторінку може дозволити браузеру пропустити деякі завантаження, але сам документ все одно потрібно перебудувати: обробити HTML, виконати скрипти, запустити фреймворк, створити DOM, відновити підписки та надіслати нові запити.
bfcache зберігає всю сторінку, включаючи її DOM та масив JavaScript. Коли користувач відходить, виконання скриптів може бути призупинено; коли він повертається через історію перегляду, браузер відображає збережену сторінку та продовжує її роботу. Саме тому успішний запит до bfcache кращий навіть за добре налаштоване оновлення, яке значною мірою надсилається з кешу HTTP: просто залишається значно менше роботи.
Це змінює підхід до мислення щодо навігації. Під час відновлення bfcache ваш звичайний шлях запуску не обов’язково виконується знову. Змінна на рівні модуля все ще може зберігати своє попереднє значення. DOM все ще відображає те, що робив користувач перед виходом. Таймери та інша діяльність під час виконання програми продовжують роботу з того місця, де зупинилися, а не створюються з нуля.
Код, побудований на припущенні, що повернення завжди означає повне перезавантаження, може пропустити оновлення, залишати на екрані застарілий стан або створювати дублікати під час спроби налаштувати те, що вже існує. Ментальна модель, яку потрібно прийняти, проста: браузер не перерендрував стару HTML-відповідь; він просто активував призупинену сторінку.
Велика перевага, яка майже не вимагає нового інтерфейсу
Більшість покращень продуктивності мають видиму ціну: перероблені компоненти, нові стани завантаження, обробка на сервері чи інша стратегія отримання даних. bfcache є незвичайним, тому що браузер вже реалізує цей механізм.
Ваше завдання — не створювати власний кеш. Його мета — уникнути блокування можливості браузера використовувати наявний кеш та зробити життєвий цикл вашої сторінки сумісним із її відновленням.
Це робить його однією з найефективніших оптимізацій, доступних багатьом командам. Сторінка, відновлена з пам’яті, уникає повторних передач даних та більшої частини обробки, необхідної під час звичайного навігування. Користь є найбільшою на повільних пристроях та мережах, але навіть на швидких пристроях помітна різниця між повним перебудовуванням сторінки та її простим відображенням.
Він також зберігає сам досвід використання, а не лише час його виконання. Уявіть собі людину, яка знаходиться на середині довгої статті, відкриває пов’язану посилання та потім повертається назад. Звичайне перезавантаження змушує перебудувати сторінку та потім намагатися відновити положення прокрутки, часто після зміни її макету. Відновлення за допомогою bfcache повертає користувача майже точно туди, де він був.
Описи продуктів ще чіткіше ілюструють цей момент. Хтось використовує чотири-п’ять фільтрів, щоб звузити каталог, просувається по довгому списку, обирає певний продукт та потім повертається назад. Якщо список відразу ж з’являється у тому самому стані, процес здається безперервним. Якщо відбувається перезавантаження, повторне отримання даних, скидання фільтрів або повернення до першого рядка, сайт здається крихким, незалежно від того, наскільки гарними є показники першого завантаження.
Продуктивність — це не лише швидкість, з якою користувачі заходять у додаток. Це також кількість зусиль, яку продукт допомагає їм заощадити під час переміщення між екранами.
Як звичка виконувати операцію unload тихо позбавляє користувачів переваг
Однією з причин, чому bfcache залишається поза увагою, є те, що коли сторінка не підходить для зберігання, нічого не відмовляє напряму. Браузер просто виконує звичайну навігацію по історії. Усе все ще працює; користувачі просто втрачають можливість миттєвого відновлення.
Класичним винуватцем є подія unload. Старіші версії коду використовують її для надсилання аналітики, звільнення ресурсів чи повідомлення серверу про закриття сторінки, ґрунтуючись на припущенні, що навігація завжди знищує сторінку та дає останній шанс її очистити.
Ця теорія більше не відповідає реальності. Сторінку можна призупинити для bfcache замість того, щоб видалити, а функція unload вже давно є ненадійною, особливо в мобільних пристроях, де вона часто зовсім не спрацьовує. Поведінка відрізняється у різних браузерах, але реєстрація слухача unload може у деяких з них унеможливити використання сторінки для bfcache. Текущі рекомендації платформи настійно не радять використовувати unload та пропонують події життєвого циклу, які відповідають сучасній моделі. На момент написання Chrome також знаходиться у процесі видалення підтримки unload, тому перевіряйте останню інформацію у документації до вашого браузера.
Функція-прослуховувач навіть не обов’язково має походити з вашого власного коду. Фрагменти аналітики, теги реклами, віджети чат-підтримки та старі залежності іноді реєструють таку функцію внутрішньо. Вона проходить перевірку коду, оскільки не має видимого впливу, проте може скасувати оптимізацію на рівні браузера під час кожного відвідування цієї сторінки.
Заміна функції unload — це не просто зміна назви події. Правильний замінник залежить від того, для чого насправді призначений код:
- Дані аналітики можна надсилати за допомогою
navigator.sendBeacon()або налаштованого запитуfetch(наприклад, з параметромkeepalive) коли видимість сторінки змінюється на «приховано». - Попередження про незбережені дані форми можна використовувати з функцією
beforeunload, але цей прослуховувач слід додавати лише тоді, коли є незбережені зміни, і видаляти після їх збереження, а не залишати його протягом усього життєвого циклу сторінки.
Загальний урок: виходження зі сторінки — це не кінець її життя. Код, побудований на цій ідеї, виявляється менш надійним та повільнішим, ніж це необхідно.
Виявлення відновлення за допомогою 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 вказує конкретні причини та визначає, які фрейми є причиною цього.
Включіть цю перевірку до регулярних тестів продуктивності важливих сторінок, особливо результатів пошуку, статей, списків товарів, панелей керування та будь-яких екранів, які користувачі часто відвідують. Також запускайте її з увімкненими скриптами сторонніх розробників, адже можливість використання bfcache часто блокується кодом ззовні вашого власного пакету.
Щодо даних з філду, інформація notRestoredReasons, яка надається через API Performance (у записі про час навігації), може пояснити, чому реальні історії навігації не були відновлені. Підтримка цього механізму не є універсальною, тому слід розглядати його як інструмент поступової діагностики, а не щось, від чого залежить ваш код у різних браузерах. Він все одно є корисним, адже жоден локальний тест не може відтворити кожну розширення браузера, сторонній iframe, відкрите з’єднання, заголовки чи умови браузера, які мають користувачі. Легкою альтернативою є запис значення event.persisted від події pageshow у вашому аналітичному забезпеченні для вимірювання фактичного рівня відвідуваності.
Не очікуйте, що кожна навігація по історії буде зберігатися у кеші. Браузери видаляють знімки стану для керування пам’яттю, а деякі фактори, що перешкоджають цьому, знаходяться поза контролем сторінки. Реалістичною метою є усунення тих факторів, які можна уникнути, та правильне відновлення стану у разі його зміни.
Підсумок
bfcache легко проігнорувати саме тому, що ніщо у вашому кодовому базисі не активує його. Не існує жодного пакета, компонента чи файлу конфігурації, які б робили його частиною архітектури; браузер просто намагається забезпечити користувачам кращий досвід. Ця невидимість має двосторонні наслідки: коли вона працює, користувачі отримують майже миттєву навігацію по історії майже без додаткового коду; коли вона блокується, сайт уповільнюється без жодної помилки; а коли відновлення відбувається неправильно, з’являється застарілий стан, оскільки додаток припускає, що повернення завжди означає перезавантаження.
Практичний чек-лист для найчастіше відвідуваних сторінок:
- Видаліть слухачі подій
unload, включаючи ті, що додані скриптами сторонніх розробників, та перенесіть їхню роботу доpagehide,visibilitychangeабоsendBeacon(). - Приєднуйте обробник події
beforeunloadлише тоді, коли є незбережені зміни. - Закрийте або призупиніть зовнішні з’єднання у функції
pagehideта знову підключіться після відновлення сторінки. - Використовуйте
pageshowразом із параметромevent.persisted, щоб оновлювати лише ті дані, які можуть устаріти, та зберігайте перевірки авторизації на сервері. - Запустіть тест кешу в інструментах розробника «назад/вперед» та відстежуйте реальні показники відновлення даних, де це дозволяють API.
Основна зміна полягає у способі уявлення про сторінку. Замість однієї прямої лінії від створення до знищення її життєвий цикл може включати налаштування, приховування, заморожування, відновлення, узгодження та продовження роботи. Команди докладають багато зусиль для прискорення реконструкції сторінок, тоді як браузер часто може повністю пропустити цей процес та повернути користувачам сторінку, яку вони вже мали.