Поддержка браузера
Современные браузеры сегодня иногда приостанавливают страницы или полностью удаляют их, когда системные ресурсы ограничены. В будущем браузеры захотят делать это заранее, чтобы потреблять меньше энергии и памяти. API жизненного цикла страницы предоставляет перехватчики жизненного цикла, поэтому ваши страницы могут безопасно обрабатывать эти вмешательства браузера, не влияя на взаимодействие с пользователем. Взгляните на API, чтобы узнать, следует ли вам реализовывать эти функции в своем приложении.
Фон
Жизненный цикл приложения — это ключевой способ управления ресурсами современных операционных систем. В Android, iOS и последних версиях Windows приложения могут запускаться и останавливаться в любой момент операционной системой. Это позволяет этим платформам оптимизировать и перераспределять ресурсы там, где они принесут максимальную пользу пользователю.
В сети исторически не было такого жизненного цикла, и приложения можно поддерживать бесконечно. При большом количестве запущенных веб-страниц критические системные ресурсы, такие как память, процессор, батарея и сеть, могут быть перегружены, что приводит к ухудшению качества работы конечных пользователей.
Хотя веб-платформа уже давно имеет события, связанные с состояниями жизненного цикла, такие как load
, unload
и visibilitychange
, эти события позволяют разработчикам реагировать только на изменения состояния жизненного цикла, инициированные пользователем. Чтобы Интернет надежно работал на устройствах с низким энергопотреблением (и в целом более экономно расходовал ресурсы на всех платформах), браузерам необходим способ активного освобождения и перераспределения системных ресурсов.
Фактически, сегодня браузеры уже принимают активные меры для экономии ресурсов страниц в фоновых вкладках, и многие браузеры (особенно Chrome) хотели бы делать это гораздо больше — чтобы уменьшить общее потребление ресурсов.
Проблема в том, что у разработчиков нет возможности подготовиться к такого рода вмешательствам со стороны системы или даже знать, что они происходят. Это означает, что браузеры должны быть консервативными, иначе они рискуют сломать веб-страницы.
API жизненного цикла страницы пытается решить эту проблему следующим образом:
- Внедрение и стандартизация концепции состояний жизненного цикла в Интернете.
- Определение новых, инициируемых системой состояний, которые позволяют браузерам ограничивать ресурсы, которые могут использоваться скрытыми или неактивными вкладками.
- Создание новых API и событий, которые позволяют веб-разработчикам реагировать на переходы в эти новые состояния, инициируемые системой, и обратно.
Это решение обеспечивает предсказуемость, необходимую веб-разработчикам для создания приложений, устойчивых к вмешательствам системы, и позволяет браузерам более агрессивно оптимизировать системные ресурсы, что в конечном итоге приносит пользу всем веб-пользователям.
В оставшейся части статьи будут представлены новые функции жизненного цикла страницы и рассмотрено, как они связаны со всеми существующими состояниями и событиями веб-платформы. В нем также будут даны рекомендации и лучшие практики для типов работы, которую разработчики должны (и не должны) выполнять в каждом штате.
Обзор состояний и событий жизненного цикла страницы
Все состояния жизненного цикла страницы являются дискретными и взаимоисключающими, то есть страница может находиться только в одном состоянии одновременно. И большинство изменений в состоянии жизненного цикла страницы обычно можно наблюдать через события DOM (исключения см. в рекомендациях разработчика для каждого состояния ).
Возможно, самый простой способ объяснить состояния жизненного цикла страницы, а также события, которые сигнализируют о переходах между ними, — это использовать диаграмму:
Штаты
В следующей таблице подробно объясняется каждое состояние. В нем также перечислены возможные состояния, которые могут возникнуть до и после, а также события, которые разработчики могут использовать для наблюдения за изменениями.
Состояние | Описание |
---|---|
Активный | Страница находится в активном состоянии, если она видима и имеет фокус ввода. Возможные предыдущие состояния: Возможные следующие состояния: |
Пассивный | Страница находится в пассивном состоянии, если она видима и не имеет фокуса ввода. Возможные предыдущие состояния: Возможные следующие состояния: |
Скрытый | Страница находится в скрытом состоянии, если она не видна (и не была заморожена, удалена или закрыта). Возможные предыдущие состояния: Возможные следующие состояния: |
Замороженный | В замороженном состоянии браузер приостанавливает выполнение замораживаемых задач в очередях задач страницы до тех пор, пока страница не будет разморожена. Это означает, что такие вещи, как таймеры JavaScript и обратные вызовы выборки, не запускаются. Уже запущенные задачи могут завершиться (что наиболее важно, обратный вызов Браузеры замораживают страницы, чтобы сохранить использование процессора, батареи и данных; они также делают это для ускорения навигации вперед и назад , избегая необходимости полной перезагрузки страницы. Возможные предыдущие состояния: Возможные следующие состояния: |
Прекращено | Страница находится в завершенном состоянии, когда она начала выгружаться и очищаться из памяти браузером. В этом состоянии не могут запускаться новые задачи , а выполняющиеся задачи могут быть прекращены, если они выполняются слишком долго. Возможные предыдущие состояния: Возможные следующие состояния: |
Выброшено | Страница находится в удаленном состоянии, когда она выгружается браузером в целях экономии ресурсов. Никакие задачи, обратные вызовы событий или JavaScript любого типа не могут выполняться в этом состоянии, поскольку сбросы обычно происходят из-за ограничений ресурсов, когда запуск новых процессов невозможен. В отброшенном состоянии сама вкладка (включая заголовок и значок вкладки) обычно видна пользователю, даже если страница исчезла. Возможные предыдущие состояния: Возможные следующие состояния: |
События
Браузеры отправляют множество событий, но лишь небольшая часть из них сигнализирует о возможном изменении состояния жизненного цикла страницы. В следующей таблице описаны все события, относящиеся к жизненному циклу, и перечислены состояния, в которые они могут переходить и из которых они могут переходить.
Имя | Подробности |
---|---|
focus | Элемент DOM получил фокус. Примечание. Событие Возможные предыдущие состояния: Возможные текущие состояния: |
blur | Элемент DOM потерял фокус. Примечание. Событие Возможные предыдущие состояния: Возможные текущие состояния: |
visibilitychange | Значение |
freeze * | Страница только что была заморожена. Любая замораживаемая задача в очередях задач страницы не будет запущена. Возможные предыдущие состояния: Возможные текущие состояния: |
resume * | Браузер возобновил работу зависшей страницы. Возможные предыдущие состояния: Возможные текущие состояния: |
pageshow | Выполняется переход к записи истории сеанса. Это может быть либо совершенно новая загрузка страницы, либо страница, взятая из обратного/прямого кэша . Если страница была взята из обратного/прямого кэша, свойство Возможные предыдущие состояния: |
pagehide | Просматривается запись истории сеанса. Если пользователь переходит на другую страницу и браузер может добавить текущую страницу в кэш обратной/перемотки для повторного использования позже, свойство Возможные предыдущие состояния: Возможные текущие состояния: |
beforeunload | Окно, документ и его ресурсы будут выгружены. Документ по-прежнему виден, и на этом этапе событие по-прежнему можно отменить. Важно: событие Возможные предыдущие состояния: Возможные текущие состояния: |
unload | Страница выгружается. Предупреждение: использование события Возможные предыдущие состояния: Возможные текущие состояния: |
* Указывает на новое событие, определенное API жизненного цикла страницы.
Новые функции, добавленные в Chrome 68
На предыдущей диаграмме показаны два состояния, которые инициируются системой, а не пользователем: заморожено и отброшено . Как упоминалось ранее, браузеры сегодня уже время от времени зависают и удаляют скрытые вкладки (по своему усмотрению), но у разработчиков нет возможности узнать, когда это происходит.
В Chrome 68 разработчики теперь могут наблюдать, когда скрытая вкладка фиксируется и размораживается, прослушивая события freeze
и resume
в document
.
document.addEventListener('freeze', (event) => {
// The page is now frozen.
});
document.addEventListener('resume', (event) => {
// The page has been unfrozen.
});
Начиная с Chrome 68, объект document
теперь включает свойство wasDiscarded
в настольном Chrome ( поддержка Android отслеживается в этой проблеме ). Чтобы определить, была ли страница удалена на скрытой вкладке, вы можете проверить значение этого свойства во время загрузки страницы (примечание: удаленные страницы необходимо перезагрузить для повторного использования).
if (document.wasDiscarded) {
// Page was previously discarded by the browser while in a hidden tab.
}
Советы о том, что важно делать в событиях freeze
и resume
, а также о том, как обрабатывать и готовиться к удалению страниц, см. в рекомендациях разработчика для каждого состояния .
В следующих нескольких разделах представлен обзор того, как эти новые функции вписываются в существующие состояния и события веб-платформы.
Как наблюдать состояния жизненного цикла страницы в коде
В активном , пассивном и скрытом состояниях можно запускать код JavaScript, который определяет текущее состояние жизненного цикла страницы на основе существующих API-интерфейсов веб-платформы.
const getState = () => {
if (document.visibilityState === 'hidden') {
return 'hidden';
}
if (document.hasFocus()) {
return 'active';
}
return 'passive';
};
С другой стороны, состояния «заморожено» и «завершено» могут быть обнаружены только в соответствующем прослушивателе событий ( freeze
и pagehide
) по мере изменения состояния.
Как наблюдать за изменениями состояния
Опираясь на определенную ранее функцию getState()
, вы можете наблюдать за всеми изменениями состояния жизненного цикла страницы с помощью следующего кода.
// Stores the initial state using the `getState()` function (defined above).
let state = getState();
// Accepts a next state and, if there's been a state change, logs the
// change to the console. It also updates the `state` value defined above.
const logStateChange = (nextState) => {
const prevState = state;
if (nextState !== prevState) {
console.log(`State change: ${prevState} >>> ${nextState}`);
state = nextState;
}
};
// Options used for all event listeners.
const opts = {capture: true};
// These lifecycle events can all use the same listener to observe state
// changes (they call the `getState()` function to determine the next state).
['pageshow', 'focus', 'blur', 'visibilitychange', 'resume'].forEach((type) => {
window.addEventListener(type, () => logStateChange(getState()), opts);
});
// The next two listeners, on the other hand, can determine the next
// state from the event itself.
window.addEventListener('freeze', () => {
// In the freeze event, the next state is always frozen.
logStateChange('frozen');
}, opts);
window.addEventListener('pagehide', (event) => {
// If the event's persisted property is `true` the page is about
// to enter the back/forward cache, which is also in the frozen state.
// If the event's persisted property is not `true` the page is
// about to be unloaded.
logStateChange(event.persisted ? 'frozen' : 'terminated');
}, opts);
Этот код делает три вещи:
- Устанавливает начальное состояние с помощью функции
getState()
. - Определяет функцию, которая принимает следующее состояние и в случае изменения регистрирует изменения состояния на консоли.
- Добавляет прослушиватели событий для всех необходимых событий жизненного цикла, которые, в свою очередь, вызывают
logStateChange()
, передавая следующее состояние.
В коде следует отметить одну вещь: все прослушиватели событий добавляются в window
и все они передают {capture: true}
. Для этого есть несколько причин:
- Не все события жизненного цикла страницы имеют одну и ту же цель.
pagehide
иpageshow
запускаются вwindow
;visibilitychange
,freeze
иresume
активируются дляdocument
, аfocus
иblur
активируются для соответствующих элементов DOM. - Большинство этих событий не всплывают, а это означает, что невозможно добавить не фиксирующие прослушиватели событий к общему элементу-предку и наблюдать за ними всеми.
- Фаза захвата выполняется до целевой фазы или фазы пузырька, поэтому добавление прослушивателей помогает гарантировать, что они запустятся до того, как другой код сможет их отменить.
Рекомендации разработчиков для каждого штата
Разработчикам важно понимать состояния жизненного цикла страницы и знать, как наблюдать их в коде, поскольку тип работы, которую вам следует (и не следует) выполнять, во многом зависит от того, в каком состоянии находится ваша страница.
Например, явно не имеет смысла отображать временное уведомление пользователю, если страница находится в скрытом состоянии. Хотя этот пример довольно очевиден, есть и другие рекомендации, не столь очевидные, которые стоит перечислить.
Состояние | Рекомендации разработчиков |
---|---|
Active | Активное состояние — это наиболее критическое время для пользователя и, следовательно, самое важное время для того, чтобы ваша страница реагировала на ввод пользователя . Любая работа, не связанная с пользовательским интерфейсом, которая может блокировать основной поток, должна быть отнесена к периодам простоя или переложена на веб-воркера . |
Passive | В пассивном состоянии пользователь не взаимодействует со страницей, но все равно может ее видеть. Это означает, что обновления пользовательского интерфейса и анимация по-прежнему должны быть плавными, но время появления этих обновлений менее критично. Когда страница меняется с активной на пассивную , самое время сохранить несохраненное состояние приложения. |
Когда страница меняется с пассивной на скрытую , возможно, пользователь больше не будет с ней взаимодействовать, пока она не будет перезагружена. Переход в скрытое состояние также часто является последним изменением состояния, которое надежно отслеживается разработчиками (это особенно актуально для мобильных устройств, поскольку пользователи могут закрывать вкладки или само приложение браузера, а события Это означает, что вы должны рассматривать скрытое состояние как вероятное завершение сеанса пользователя. Другими словами, сохраните любое несохраненное состояние приложения и отправьте все неотправленные аналитические данные. Вам также следует прекратить делать обновления пользовательского интерфейса (поскольку они не будут видны пользователю), а также остановить все задачи, которые пользователь не хочет запускать в фоновом режиме. | |
Frozen | В замороженном состоянии замораживаемые задачи в очередях задач приостанавливаются до тех пор, пока страница не будет разморожена, чего может никогда не произойти (например, если страница будет удалена). Это означает, что когда страница меняется со скрытой на замороженную, важно остановить все таймеры или разорвать любые соединения, которые в случае заморозки могут повлиять на другие открытые вкладки в том же источнике или повлиять на способность браузера помещать страницу на задний план. вперед кэш . В частности, важно, чтобы вы:
Вам также следует сохранить любое динамическое состояние представления (например, положение прокрутки в представлении бесконечного списка) в Если страница снова переходит из замороженного состояния в скрытую , вы можете повторно открыть все закрытые соединения или перезапустить любой опрос, который вы остановили, когда страница была первоначально заморожена. |
Terminated | Обычно вам не нужно предпринимать никаких действий, когда страница переходит в состояние завершения . Поскольку страницы, выгружаемые в результате действий пользователя, всегда проходят скрытое состояние перед переходом в состояние завершения , в скрытом состоянии должна выполняться логика завершения сеанса (например, сохранение состояния приложения и отчетность для аналитики). Кроме того (как упоминалось в рекомендациях по скрытому состоянию ), разработчикам очень важно понимать, что переход в завершенное состояние не может быть надежно обнаружен во многих случаях (особенно на мобильных устройствах), поэтому разработчики, которые зависят от событий завершения (например, |
Discarded | Отброшенное состояние не наблюдается разработчиками в момент удаления страницы. Это связано с тем, что страницы обычно отбрасываются из-за ограничений ресурсов, и разморозить страницу только для того, чтобы разрешить запуск сценария в ответ на событие удаления, в большинстве случаев просто невозможно. В результате вам следует подготовиться к возможности сброса при изменении скрытой на замороженной страницы , а затем вы сможете отреагировать на восстановление удаленной страницы во время загрузки страницы, проверив |
Еще раз: поскольку надежность и порядок событий жизненного цикла не реализованы последовательно во всех браузерах, самый простой способ следовать советам в таблице — использовать PageLifecycle.js .
Устаревшие API жизненного цикла, которых следует избегать
По возможности следует избегать следующих событий.
Событие выгрузки
Многие разработчики рассматривают событие unload
как гарантированный обратный вызов и используют его как сигнал окончания сеанса для сохранения состояния и отправки аналитических данных, но делать это крайне ненадежно , особенно на мобильных устройствах! Событие unload
не срабатывает во многих типичных ситуациях выгрузки, включая закрытие вкладки с помощью переключателя вкладок на мобильном устройстве или закрытие приложения браузера с помощью переключателя приложений.
По этой причине всегда лучше полагаться на событие visibilitychange
, чтобы определить момент завершения сеанса, и рассматривать скрытое состояние как последнее надежное время для сохранения данных приложения и пользователя .
Более того, само наличие зарегистрированного обработчика событий unload
(через onunload
или addEventListener()
) может помешать браузерам помещать страницы в кэш обратной/перемотки для более быстрой загрузки назад и вперед.
Во всех современных браузерах рекомендуется всегда использовать событие pagehide
для обнаружения возможных выгрузок страницы (так называемое состояние завершения ), а не событие unload
. Если вам нужна поддержка Internet Explorer версии 10 и ниже, вам следует определить событие pagehide
и использовать unload
только в том случае, если браузер не поддерживает pagehide
:
const terminationEvent = 'onpagehide' in self ? 'pagehide' : 'unload';
window.addEventListener(terminationEvent, (event) => {
// Note: if the browser is able to cache the page, `event.persisted`
// is `true`, and the state is frozen rather than terminated.
});
Событие перед разгрузкой
Событие beforeunload
имеет ту же проблему, что и событие unload
, поскольку исторически наличие события beforeunload
могло препятствовать тому, чтобы страницы были пригодны для обратного/прямого кэширования . Современные браузеры не имеют этого ограничения. Хотя некоторые браузеры в качестве меры предосторожности не запускают событие beforeunload
при попытке поместить страницу в обратный/прямой кеш, что означает, что это событие не является надежным сигналом об окончании сеанса. Кроме того, некоторые браузеры (включая Chrome ) требуют взаимодействия пользователя со страницей, прежде чем разрешить запуск события beforeunload
, что еще больше влияет на его надежность.
Одно из различий между beforeunload
и unload
заключается в том, что существует законное использование beforeunload
. Например, если вы хотите предупредить пользователя о том, что у него есть несохраненные изменения, которые они потеряют, если продолжат выгружать страницу.
Поскольку существуют веские причины для использования beforeunload
, рекомендуется добавлять прослушиватели beforeunload
только в том случае, если у пользователя есть несохраненные изменения, а затем удалять их сразу после их сохранения.
Другими словами, не делайте этого (поскольку при этом прослушиватель beforeunload
добавляется безоговорочно):
addEventListener('beforeunload', (event) => {
// A function that returns `true` if the page has unsaved changes.
if (pageHasUnsavedChanges()) {
event.preventDefault();
// Legacy support for older browsers.
return (event.returnValue = true);
}
});
Вместо этого сделайте это (поскольку он добавляет прослушиватель beforeunload
только тогда, когда это необходимо, и удаляет его, когда это не так):
const beforeUnloadListener = (event) => {
event.preventDefault();
// Legacy support for older browsers.
return (event.returnValue = true);
};
// A function that invokes a callback when the page has unsaved changes.
onPageHasUnsavedChanges(() => {
addEventListener('beforeunload', beforeUnloadListener);
});
// A function that invokes a callback when the page's unsaved changes are resolved.
onAllChangesSaved(() => {
removeEventListener('beforeunload', beforeUnloadListener);
});
Часто задаваемые вопросы
Почему нет состояния «загрузка»?
API жизненного цикла страницы определяет состояния как дискретные и взаимоисключающие. Поскольку страница может быть загружена либо в активном, либо в пассивном, либо в скрытом состоянии и поскольку она может менять состояния (или даже завершаться) до завершения загрузки, отдельное состояние загрузки не имеет смысла в рамках этой парадигмы.
Моя страница выполняет важную работу, когда она скрыта. Как я могу предотвратить ее заморозку или удаление?
Существует множество законных причин, по которым веб-страницы не следует блокировать, пока они работают в скрытом состоянии. Самый очевидный пример — приложение, воспроизводящее музыку.
Существуют также ситуации, когда для Chrome было бы рискованно удалять страницу, например, если она содержит форму с неотправленным пользовательским вводом или если у нее есть обработчик beforeunload
, который предупреждает, когда страница выгружается.
На данный момент Chrome будет консервативен при удалении страниц и будет делать это только тогда, когда будет уверен, что это не повлияет на пользователей. Например, страницы, на которых было замечено выполнение любого из следующих действий в скрытом состоянии, не будут удалены, за исключением случаев крайней ограниченности ресурсов:
- Воспроизведение аудио
- Использование WebRTC
- Обновление заголовка таблицы или значка
- Показ оповещений
- Отправка push-уведомлений
Информацию о текущих функциях списка, используемых для определения того, можно ли безопасно заморозить или удалить вкладку, см. в разделе Эвристика замораживания и удаления в Chrome.
Что такое обратный/прямой кеш?
Кэш назад/вперед — это термин, используемый для описания оптимизации навигации, реализованной в некоторых браузерах, которая ускоряет использование кнопок «Назад» и «Вперед».
Когда пользователь уходит со страницы, эти браузеры замораживают версию этой страницы, чтобы ее можно было быстро возобновить, если пользователь вернется назад с помощью кнопок «Назад» или «Вперед». Помните, что добавление обработчика событий unload
делает эту оптимизацию невозможной .
По сути, это зависание функционально аналогично тому, как зависают браузеры для экономии ресурсов ЦП/батареи; по этой причине он считается частью состояния замороженного жизненного цикла.
Если я не могу запускать асинхронные API в замороженном или прекращенном состоянии, как мне сохранить данные в IndexedDB?
В состояниях «заморожено» и «завершено» замораживаемые задачи в очередях задач страницы приостанавливаются, а это означает, что асинхронные API и API на основе обратных вызовов, такие как IndexedDB, не могут быть надежно использованы.
В будущем мы добавим метод commit()
к объектам IDBTransaction
, который даст разработчикам возможность выполнять транзакции, которые фактически предназначены только для записи и не требуют обратных вызовов. Другими словами, если разработчик просто записывает данные в IndexedDB и не выполняет сложную транзакцию, состоящую из чтения и записи, метод commit()
сможет завершиться до приостановки очереди задач (при условии, что база данных IndexedDB уже открыта).
Однако для кода, который должен работать сегодня, у разработчиков есть два варианта:
- Использовать хранилище сеансов. Хранилище сеансов является синхронным и сохраняется при удалении страниц.
- Используйте IndexedDB у вашего сервис-воркера: сервис-воркер может хранить данные в IndexedDB после того, как страница была закрыта или удалена. В прослушивателе событий
freeze
илиpagehide
вы можете отправлять данные своему сервисному работнику черезpostMessage()
, а сервисный работник может обрабатывать сохранение данных.
Тестирование вашего приложения в замороженном и отброшенном состояниях
Чтобы проверить, как ваше приложение ведет себя в замороженном и удаленном состояниях, вы можете посетить chrome://discards
чтобы фактически заморозить или удалить любую из открытых вкладок.
Это позволяет вам убедиться, что ваша страница правильно обрабатывает события freeze
и resume
а также флаг document.wasDiscarded
при перезагрузке страниц после удаления.
Краткое содержание
Разработчики, которые хотят бережно относиться к системным ресурсам устройств своих пользователей, должны создавать свои приложения с учетом состояний жизненного цикла страницы. Крайне важно, чтобы веб-страницы не потребляли чрезмерно системные ресурсы в ситуациях, которых пользователь не ожидает.
Чем больше разработчиков начнут внедрять новые API жизненного цикла страниц, тем безопаснее будет для браузеров блокировать и удалять страницы, которые не используются. Это означает, что браузеры будут потреблять меньше памяти, процессора, аккумулятора и сетевых ресурсов, что является преимуществом для пользователей.