PageLifecycle
PageLifecycle
PageLifecycle is a small utility that exposes the browser page lifecycle as an Observable.
It normalizes multiple browser events (visibilitychange, focus, pagehide, freeze, etc.) into a single, consistent state machine, making it easy to react to how a page is being used, backgrounded, frozen, or terminated.
This is especially useful for offline-first apps, media playback, P2P / realtime apps, and any application that needs accurate lifecycle awareness.
States
| State | Description |
|---|---|
active |
Page is visible and has focus |
passive |
Page is visible but does not have focus |
hidden |
Page is not visible |
frozen |
Page has been frozen by the browser |
terminated |
Page is unloading or being navigated away from |
Note: These states are designed to mirror the Service Worker
Client.activeStateenums (active,idle,hidden, etc.) as closely as possible. This makes it easier to keep your SW and page logic consistent.
How it works
- Extends
Observable - Lifecycle logic is injected directly via
super(...) - A single generic event listener handles all lifecycle events
- State is derived from browser primitives (
document.visibilityState,document.hasFocus()) - Emits only when the state actually changes
- Cleanup is handled via AbortSignal
- Attempts to align with Service Worker client states
Current state
The current lifecycle state is always available synchronously via the state property.
pageLifecycle.state
This allows you to:
- read the lifecycle state without subscribing
- initialize logic based on the current state
- safely combine imperative and reactive code
Example:
if (lifecycle.state === 'active') {
start()
}
Example
Basic usage (ESM via URL)
import { PageLifecycle } from 'https://jimmy.warting.se/packages/packages/page-lifecycle/index.js'
const lifecycle = new PageLifecycle()
lifecycle.subscribe(({ oldState, newState }) => {
console.log(`${oldState} → ${newState}`)
})
Pause / resume logic
lifecycle.subscribe(({ newState }) => {
if (newState === 'hidden' || newState === 'frozen') {
pause()
}
if (newState === 'active') {
resume()
}
})
Cleanup (AbortSignal)
PageLifecycle does not provide an unsubscribe() method.
Instead, cleanup is controlled using an AbortSignal, which is forwarded to all internal event listeners.
const controller = new AbortController()
lifecycle.subscribe(
({ newState }) => {
console.log(newState)
},
{ signal: controller.signal }
)
// later
controller.abort()
When the signal is aborted:
- all internal event listeners are removed
- no further lifecycle events are emitted
Design goals
- Observable-first API
- AbortSignal-based lifecycle control
- No polling
- No duplicate emissions
- Minimal overhead
- Browser-native lifecycle semantics
- Mirrors Service Worker
Client.activeStateenums
Further reading & tips to avoid tab freezing
Tab freezing is controlled by the browser (especially Chrome) to save CPU and battery. There are some resources and techniques that can help you understand or potentially reduce freezing:
- Chrome Page Lifecycle API – official docs on page lifecycle events
chrome://discards/– inspect why tabs may be frozen; hints: active audio, WebRTC, WebSocket, Web Push- How Chrome’s Tab Freezing Saves CPU & Battery – article explaining Chrome’s logic
chrome://site-engagement/– increase site engagement score (set to 100) to indicate frequent use- Installing the page as a PWA can help; configure the app to allow background processes and disable battery-saver restrictions
- https://www.itechtics.com/disable-tab-throttling/
-
⚠️ Note: These are hints and cannot guarantee a tab will never be frozen; browsers ultimately control freezing policies.
Export
The module uses named exports.
export { PageLifecycle }