The original link

background

Recently, I have been analyzing web performance collection, and I have always felt that it is unreasonable to put collection not related to user interaction in onLoad or DOMContentLoaded. A search reveals that Web pages also have a life cycle. Some research has solved the confusion of how to avoid interfering with users’ information collection. Page Lifecycle, the latest W3C specification, provides a series of Lifecycle hook functions that allow developers to listen for operations without interfering with user interactions.

Question: How to use the lifecycle to gracefully process the reported analysis data, so as to ensure that it is not missed in some scenarios, and to minimize the user’s interference?

The profile

Application life cycle is a key approach to resource management in modern operating systems. In mobile iOS, Android and the latest desktop systems, apps can be started or closed by OS at any time. The life cycle makes these systems streamline and reallocate resources in a more reasonable and efficient way, which greatly optimizes user experience.

Historically, the Web has no concept of a life cycle, so Web applications can live and occupy system resources forever. A large number of Tab pages are opened on the browser. Key system resources, such as memory, CPU, battery, and network, are occupied and cannot be released. As a result, the system is stuck. For example, the old version of Chrome, although the performance of the browser at the time of the single page execution comparison has always been the best, but open too many pages, especially eat memory, thanks to the life cycle, can reasonably reclaim memory.

Web platforms have long had life-cycle state-related events, such as Load, Unload, and VisibilityChange, that allow developers to listen for life-cycle state changes. For mobile devices, especially low-end models, browsers need a way to proactively reclaim and reallocate memory.

In fact, today’s browsers have taken aggressive steps to save resources on background tabs, and many browsers want to do more to reduce their resource footprint.

The problem is that developers currently have no way of preparing for these types of system startup interventions, or even knowing that they are happening. This means that browsers need to be conservative or they risk breaking the page.

The Page Lifecycle API attempts to address these issues by:

  1. Introduce and standardize the concept of life cycle states on the Web.
  2. Defines a new system startup state that allows the browser to restrict the resources available to hidden or deactivated tabs.
  3. Create new APIs and events that allow Web developers to respond to transitions between these new system startup states.

This solution provides the predictability that Web developers need to build applications that are resilient to system intervention and allows browsers to more aggressively optimize system resources, ultimately benefiting all Web users.

This article introduces the new page life cycle features and explores their relationship to all existing Web platform states and events. It will also provide advice and best practices for the types of work developers should (and shouldn’t) do in each state.

Life cycle states and events

All page life cycle states are discrete and mutually exclusive, meaning that a page can only be in one state at a time. Most changes to the page lifecycle state are typically observed through DOM events (for exceptions, see developer recommendations for each state).

Lifecycle state transitions and triggered events

state

state describe Possible previous state (trigger event) Possible next state (trigger event)
Active The page is visibledocument.visibilityState === 'visible'And there areinput focus 1. passive (focus) 1. passive (blur)
Passive The page is visible and no input is in focus 1. active (blur)

2. hidden (visibilitychange)
1. active (focus)

2. hidden (visibilitychange)
Hidden Page not visibledocument.visibilityState === 'hidden'And not frozen 1. passive (visibilitychange) 1. passive (the visibilitychange)

2. frozen (freeze)

3. terminated (pagehide)
Frozen frozenThe status browser suspends execution of freezable tasks in the task queue, which means for exampleJS timerorfetchThe callback will not be executed. Ongoing tasks can be completed, but the actions that can be performed and the running time are limited.



The browser freezes to save CPU, memory, and power consumption. It also makes moving forward and backward faster and avoids reloading full pages from the network
1. hidden (freeze) 1. active (resume -> pageshow)

2. passive (resume -> pageshow)

3. hidden (resume)
Terminated terminatedStatus Indicates that the browser unloads the page and reclaims the occupied resources. No new tasks are executed and long tasks may be deleted. 1. hidden (pagehide) There is no
Discarded discardedState occurs when system resources are limited and the browser actively unloads pages to free up memory and other resources for new entries/threads. No task, event callback, or JS of any type can be executed in this state. Even though the page is gone, the Tab name and Favicon of the browser Tab page are still visible to the user 1. frozen (no events fired) There is no

The event

The following describes all of the events related to the life cycle and lists the states they can transition to.

focus

  • Description: DOM elements get focus
  • The previous possible state
  1. passive
  • Current possible state
  1. active
  • Note: The focus event does not always trigger a life-cycle state change, only if there is no focus before the page.

blur

  • Description: DOM elements lose focus
  • The previous possible state
  1. active
  • Current possible state
  1. passive
  • Note: The blur event does not always trigger a life-cycle state change, only when the page is no longer in focus. Switching focus between page elements, for example, does not.

visibilitychange

  • Description:document.visibilityStateValue changes. Trigger scenario:
  1. Refresh or navigate to a new page
  2. Switch to a new Tab page
  3. Close Tab, minimize, or close the browser
  4. Mobile terminal switch app, such as press the Home button, click the head notification switch, etc
  • The previous possible state
  1. passive
  2. hidden
  • Current possible state
  1. passive
  2. hidden

freeze *

  • Description: The page is frozen and the freezable tasks in the task queue are not executed.
  • The previous possible state
  1. hidden
  • Current possible state
  1. frozen

resume *

  • Description: The browser restarts a frozen page
  • The previous possible state
  1. frozen
  • Current possible state
  1. active (if followed by the pageshow event)
  2. passive (if followed by the pageshow event)
  3. hidden

pageshow

  • Description: Retrieves whether the page navigation cache exists, retrieves it from the cache if it does, otherwise loads a brand new page. Event property if the page is fetched from the navigation cachepersistedIs true, or false otherwise.
  • The previous possible state
  1. Frozen (Resume event is also triggered)
  • Current possible state
  1. active
  2. passive
  3. hidden

pagehide

  • Description: Whether the page session can be stored in the navigation cache. Event property if the user navigates to another page and the browser is able to add the current page to the page navigation cache for later reusepersistedTo true. If true, the page will enterfrozenState, otherwise will enterterminatedState.
  • The previous possible state
  1. hidden
  • Current possible state
  1. frozen (event.persisted is true, freeze event follows)
  2. terminated (event.persisted is false, unload event follows)

beforeunload

  • Description: The current page is about to be uninstalled. The document content of the current page is still visible. Closing the page can be cancelled at this stage.
  • The previous possible state
  1. hidden
  • Current possible state
  1. terminated
  • Warning: MonitorbeforeunloadEvent, which is used only to alert the user to unsaved data changes and should be removed once the data has been saved. It should not be added to the page unconditionally, as doing so can hurt performance in some cases.

unload

  • Description: The page is being unloaded.
  • The previous possible state
  1. hidden
  • Current possible state
  1. terminated
  • Warning: Listening is not recommendedunloadEvent, because it is unreliable, may affect performance in some cases.
  • Represents a new event for the lifecycle definition.

New features

Frozen and discarded are system actions rather than user actions. Modern browsers may freeze or discard a page because it is invisible to a TAB page. The developer has no way of knowing how either of these things happen.

Chrome 68+ provides freeze and Resume events, which developers can listen to when a page changes from hidden to frozen and unfrozen.

document.addEventListener('freeze', (event) => {
  // The page is now frozen.
});

document.addEventListener('resume', (event) => {
  // The page has been unfrozen.
});
Copy the code

The Document. wasDiscarded property is provided to retrieve whether the currently loaded page was previously discarded when it was not visible.

if (document.wasDiscarded) {
  // Page was previously discarded by the browser while in a hidden tab.
}
Copy the code

Code observes life cycle state

Get Active, passive, and hidden

const getState = (a)= > {
  if (document.visibilityState === 'hidden') {
    return 'hidden';
  }
  if (document.hasFocus()) {
    return 'active';
  }
  return 'passive';
};
Copy the code

For example, frozen and terminated states need to listen to freeze and pagehide events to obtain.

// 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; }};// 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()), {capture: true});
});

// 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');
}, {capture: true});

window.addEventListener('pagehide', (event) => {
  if (event.persisted) {
    // If the event's persisted property is `true` the page is about
    // to enter the page navigation cache, which is also in the frozen state.
    logStateChange('frozen');
  } else {
    // If the event's persisted property is not `true` the page is
    // about to be unloaded.
    logStateChange('terminated'); }}, {capture: true});
Copy the code

The code above does three things:

  1. getState()Initialization state
  2. definelogStateChangeThe function receives the next state, if changed, console
  3. Listening to theCapture phaseEvent, one calllogStateChange, the incoming state changes.

Note: The order of the console print above may not be consistent across browsers.

  • Why pass in the third argument{capture: true}And all thewindowOn listening event
  1. Not all lifecycle events are the sametarget
    1. pagehide,pageshowwindowOn the trigger
    2. visibilitychange.freeze.resumedocumentOn the trigger
    3. focus,blurFires on the corresponding DOM element
  2. Most events do not bubble, which means that during the bubble phase, only listenwindowUnable to realize
  3. The capture phase occurs in the Target phase and the bubble phase, which means that the capture phase event is not cancelled by other bubble events

Cross-browser compatibility

Since the lifecycle API has just been introduced, the new event and DOM apis are not implemented in all browsers. In addition, all browser implementations are not consistent. Such as:

  1. Some browsers do not trigger when switching tabsblurEvent, meaningactiveState not throughpassiveState and then it goes straight tohidden
  2. Some browsers dopage navigation cache.Page Lifecycle APIClassified cached pages as frozen, but not yet implementedfreeze.resumeSuch as the latest API, although non-/ frozen states can also be passedpageshow.pagehideThe event is monitored.
  3. IE 10 and the following versions are not implementedpagehideThe event
  4. pagehide,visibilitychangeTrigger orderHas changed. When the page is being uninstalled, if the page is visible, it is triggered firstpagehideIn the triggervisibilitychange. In the latest version of Chrome, it fires first whether the page is visible or notvisibilitychangeIn the triggerpagehide.
  5. Closing the Tab page in Safari may not triggerpagehidevisibilitychange. Need to monitorbeforeunloadTo be compatible,beforeunloadYou need to end the bubble phase to know if the state becomeshiddenTherefore, it is easy to be cancelled by other events.

Pagelifecycle.js is recommended to ensure consistency across browsers.

Suggestions for each state

As a developer, it’s important to understand page lifecycle states and know how to observe them in your code, because the type of work you should (and shouldn’t) perform depends largely on what state your page is in.

For example, if the page is not visible, it clearly doesn’t make sense to show the user a temporary notification. While this example is obvious, there are some less obvious suggestions worth enumerating.

state advice
Active This state is the most important phase for the user, where the most important thing is to respond to user input. Non-no-ui tasks that block the main thread for a long time can be assignedidlePeriod orweb workerTo deal with
Passive In this state, the user is not interacting with the page, but they can still see it. This means that UI updates and animations should still be smooth, but the timing of those updates is less critical. When the page fromactiveintopassiveIs a good time to store unsaved data.
Hidden whenpassiveInto ahiddenThe user will most likely not interact with the page until it is reloaded.



hiddenState is often the last state a developer can trust, especially on mobile, such as when switching appsbeforeunload,pagehideunloadEvents are not triggered.



This means that developers should puthiddenThe state is considered the final state of the page session. In this case, unsaved application data should be persisted, collected and reported for analysis.



In the meantime, you should stop UI updates because the user can’t see them anymore. You should also stop tasks that the user doesn’t want to perform in the background, saving resources such as power.
Frozen infrozenState,Task queueIn theFreezable tasksIs suspended until the page is thawed (which may never happen, such as when the page is discarded)discarded).



At this point it is necessary to stop alltimerAnd close the connection (IndexedDB,BroadcastChannel,WebRTC,Web SocketConnections. The release ofWeb Locks), should not affect other open same-origin pages or affect the browser to store the page in the page navigation cache.



You should also persist dynamic view information (such as the slide position of an infinite slide list) tosessionStorageOr IndexedDB via commit() for reuse after discarding and reloaded.



When the state goes backhiddenYou can reopen any closed connections or restart any polling that was stopped when the page was originally frozen.
Terminated When the page becomesterminatedThe developer generally does not need to do anything. Because the user actively uninstall the page will always be interminatedExperience beforehiddenStatus (not necessarily triggered when the page refreshes and jumpsvisibilitychange, a few browsers implement it, most may need itpagehideevenbeforeunloadorunloadTo make up for these scenes), you should be inhiddenState Performs the end-of-page session logic (persisting storage, reporting analysis data).



Developers must recognize that in many cases, especially on mobile devices, termination states cannot be reliably detected and therefore rely on termination events (e.gbeforeunload,pagehideandunload) data may be lost.
Discarded The developer cannot observe the deprecated state. Because it is usually deprecated under system resource constraints, in most cases, only to allow script responsesdiscardEvent while unfreezing the page is not possible. Therefore, there is no need to go fromhiddenChange tofrozenYou can check when the page loadsdocument.wasDiscarded“To restore the pages that were previously discarded.

Avoid using old lifecycle apis

  • Unload, do not use in modern browsers
  1. A lot of developers will putunloadEvents are used to signal the end of a page to preserve status or report analysis data, but this can be very unreliable, especially on mobile.unloadThis is not triggered in many typical uninstall situations, such as switching tabs on the mobile device, closing the page, or switching the system switcher, closing the APP.
  2. Therefore, it is best to rely onvisibilitychangeEvent to determine when the page session ends and willhiddenState is the last reliable time to save application and user data.
  3. unloadPrevents the browser from placing pages in the page navigation cache, affecting the browser’s quick response to forward and backward navigation.
  4. Recommended in modern browsers (including Internet Explorer 11)pagehideEvents instead ofonloadMonitor page load (terminated).onloadIt is compatible with Internet Explorer 10 at most.
const terminationEvent = 'onpagehide' in self ? 'pagehide' : 'unload';

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.
}, {capture: true});
Copy the code
  • Beforeunload, and unload have similar problems and are only used to alert users to unsaved data when closing or jumping to a page, which is cleared immediately once saved.
// bad: use it unconditionally
addEventListener('beforeunload', (event) => {
  // A function that returns `true` if the page has unsaved changes.
  if (pageHasUnsavedChanges()) {
    event.preventDefault();
    return event.returnValue = 'Are you sure you want to exit? '; }}, {capture: true});
Copy the code
// good
const beforeUnloadListener = (event) = > {
  event.preventDefault();
  return event.returnValue = 'Are you sure you want to exit? ';
};
const unsavedChanges = [];
/** * @param {Symbol|Object} id A unique symbol or object identifying the *. pending state. This ID is required when removing the state later. */
function addUnsavedChanges(id) {
  if(unsavedChanges.indexOf(id) > - 1) return; // Repeat exit
  if (unsavedChanges.length === 0) { // First listen
    addEventListener('beforeunload', onbeforeunload);
  }
  unsavedChanges.push(id);
}
/** * @param {Symbol|Object} id A unique symbol or object identifying the *. pending state. This ID is required when removing the state later. */
function removeUnsavedChanges(id) {
  const idIndex = unsavedChanges.indexOf(id);
  if (idIndex > - 1) {
    unsavedChanges.splice(idIndex, 1);
    // If there's no more pending state, remove the event listener.
    if (unsavedChanges.length === 0) {
      removeEventListener('beforeunload', onbeforeunload); }}}Copy the code

FAQs

  • If a page is hidden and an important task is being executed, how do I prevent a page from being frozen or discarded?

There are many legitimate reasons for not freezing a page when the page is hidden, such as when an APP is playing music.

There is also a risk that the browser drops the page in some scenarios, such as when the user has unsubmitted input or when the developer listens for beforeUnload events to alert the user.

As a result, browser policies tend to be conservative, abandoning pages only when it is clear that it will not affect users. For example, the following scenario does not discard pages (unless limited by device resources).

  1. Playing audio
  2. Using WebRTC
  3. Updating the table title or favicon
  4. Showing alerts
  5. Sending push notifications

Note: For pages that update the title or Favicon to alert the user to unread notifications, it is recommended to use the Service worker, which will allow Chrome to freeze or abandon the page while still showing changes to the TAB title or Favicon.

  • What is page Navigation cache?

Page navigation cache is a generic term used to optimize back and forward button navigation and use the cache to quickly recover front and back pages. Webkit refers to Page Cache and Firefox refers to back-forward Cache (BFCache).

Freezing for CPU/ battery/memory savings and caching for quick recovery in case of heavy loads work well together. Therefore, the cache is considered part of the frozen life cycle state.

Note: beforeunload, unload prevents this item from being optimized.

  • Why is there no load, DOMContentLoaded event in the life cycle?

Page life cycle states are defined as discrete and mutually exclusive. Since pages can be loaded in active, Passive, or hidden states, separate load states are meaningless, and since the Load and DOMContentLoaded events do not indicate lifecycle state changes, they are lifecycle independent.

  • How to use asynchronous requests in frozen or terminated state

In both states, tasks may be suspended and not executed, such as asynchronous requests, callback-based apis, etc. Here are some suggestions

  1. SessionStorage, methods that are synchronous and persist data in the deprecated state.
  2. The service worker, interminated,discardedState by listeningfreeze or pagehidethroughpostMessage()Used to store data. (Limited and device resources may arouse service workers and increase device burden)
  3. The navigator.sendBeacon function can still send asynchronous requests when the running page is closed.
  • How do I view the frozen and discarded status? chrome://discards

Inspiration for analytical data acquisition timing

Compatibility analysis

  1. To avoid theload,DOMContentLoaded,beforeunload,unloadProcess and report the collected data.
  2. Listening to thevisibilitychangeProcess and collect information when switching APP and screen.
  3. Listening to thepagehidePage refresh Navigation jump scenario.
  4. Use onlybeforeunloadCompatible withSafariClose scenarios of later versions of Tab and Internet Explorer 11.
  5. Caution Once information is collected, all collection events are destroyed immediately to avoid repeated reporting.
function clear(fn) {['visibilitychange'.'pagehide'.'beforeunload']
    .forEach(event= > window.removeEventListener(event, fn, true));
}

function collect() {
  const data = { / * * / };
  const str = JSON.stringify(data);
  if('sendBeacon' in window.navigator) {
    if( window.navigator.sendBeacon(url, str) ) {
      clear(collect);
    } else {
      // Failed to send asynchronous requests}}else {
    // Todo synchronizes ajaxclear(collect); }}const isSafari = typeof safari === 'object' && safari.pushNotification;
constisIE10 = ! ('onpagehide' in window);

window.addEventListener('visibilitychange', collect, true); ! isIE10 &&window.addEventListener('pagehide', collect, true);

if(isSafari || isIE10) {
  window.addEventListener('beforeunload', collect, true);
}
Copy the code

conclusion

Performance-obsessed developers should always keep the page life cycle in mind. It is important for users not to consume device resources when they don’t need them.

In addition, the more developers start using the lifecycle APIs, the safer it will be for browsers to handle frozen or discarded pages that are no longer in use. This means the browser will consume less memory, CPU, power, and network resources, which will benefit users.

reference

  1. WebKit Page Cache
  2. Firefox Back-Forward Cache
  3. Page Lifecycle W3C
  4. Page Lifecycle API
  5. Don’t lose user and app state, use Page Visibility
  6. page-lifecycle
  7. PageLifecycle.js
  8. Lifecycle events with Page Visibility + Beacon API
  9. Why does visibilitychange fire after pagehide in the unload flow?