TL;DR, Quick Answer
6 min readThe choice between beforeunload vs pagehide matters because beforeunload does not reliably fire on mobile and, in Firefox, disqualifies a page from the back-forward cache. Pagehide fires on the same navigations without that bfcache cost, and MDN recommends visibilitychange first, pagehide as the fallback, paired with sendBeacon to flush analytics before a page goes away.
Why does beforeunload vs pagehide matter for flushing analytics?
Choosing correctly between beforeunload vs pagehide decides whether an analytics event actually reaches the server before a user leaves the page. MDN documents beforeunload as not reliably fired, especially on mobile: a user who switches to another app and later closes the browser from the app manager never fires the event at all. Use pagehide or visibilitychange to flush data instead of beforeunload, and reserve beforeunload for the one job it still does, warning a user about unsaved changes. Getting this wrong is one of the quieter reasons a real user monitoring setup undercounts exits.
What does the beforeunload event actually do?
The beforeunload event fires just before a page unloads and can show a browser-native confirmation dialog to warn a user about unsaved changes. MDN's own guidance is to add the listener only when there are unsaved changes to protect, then remove it once those changes are saved, instead of leaving it attached for the life of the page. That narrow use case is also the only one it is still suited for, since its reliability as a general-purpose exit signal is not there.

Why is beforeunload unreliable on mobile?
Beforeunload is unreliable on mobile because a browser closed from the task switcher, or an app backgrounded and never reopened, skips the event entirely, which MDN calls out directly with the app-switch-then-close scenario. A desktop tab closed with the window controls fires the event; the same sequence on a phone, backgrounding the app and closing it from the app manager, skips the event entirely. Any analytics call that depends solely on beforeunload firing loses data on exactly the platform where sessions end this way.
- User clicks the window controls
- Beforeunload fires
- User backgrounds the app, then closes it from the app manager
- Beforeunload never fires
How does beforeunload affect the back-forward cache?
Beforeunload's effect on the back-forward cache differs by browser: Firefox will not place a page in bfcache if it has a beforeunload listener attached, while web.dev's bfcache guide notes that beforeunload no longer disqualifies a page from bfcache in other modern browsers, though it did previously. web.dev still calls the event "unreliable, so avoid using it unless absolutely necessary," and recommends adding the listener conditionally, only while unsaved changes exist, instead of on every page load. A page that skips bfcache reloads from scratch on a back navigation instead of restoring instantly from memory.

What does the pagehide event do differently?
The pagehide event fires when the browser hides the current page while presenting a different page from session history, such as a click on the back button, and unlike beforeunload and unload, a pagehide listener does not make a page ineligible for bfcache. MDN recommends visibilitychange as the more reliable signal first, with pagehide as the fallback for browsers where visibilitychange is not available. Attach the flush logic to pagehide when a page needs a bfcache-safe listener that still fires on the same navigation moments beforeunload was meant to catch, which is also the moment a session timeout clock would otherwise keep running against a page nobody is looking at.
| Event | Fires reliably on mobile? | Blocks bfcache? | Best use |
|---|---|---|---|
| unload | No, MDN calls it "extremely unreliable" on mobile | Yes, on desktop Chrome and Firefox | Avoid; legacy code only |
| beforeunload | No, per MDN's app-switch example | Not in modern browsers, per web.dev | Warn about unsaved changes only |
| pagehide | Fallback signal per MDN | No | Flush analytics when visibilitychange is unavailable |
| visibilitychange | MDN's recommended primary signal | No | Flush analytics on tab hide, first choice |
Where does sendBeacon fit alongside pagehide?
sendBeacon fits alongside pagehide as the delivery mechanism, since it queues an asynchronous POST request that the browser keeps trying to send even as the page goes away, without delaying the navigation the user is already making. MDN recommends pairing it with the visibilitychange event as the primary trigger and pagehide as the fallback, instead of firing it from unload or beforeunload. A single sendBeacon call is capped at roughly 64 KiB; a payload larger than that needs fetch() with the keepalive option instead.
How should an analytics script combine these events?
An analytics script should listen for visibilitychange and check for document.visibilityState === "hidden" as the primary flush trigger, add pagehide as a fallback for browsers or situations where visibilitychange does not fire, and call sendBeacon in both handlers to send the payload without blocking navigation. Flowsery's lightweight script ships under 10 KB precisely so this listener logic adds negligible weight to a page that is already trying to leave. Skipping straight to beforeunload for this job is the one shortcut that costs the most data on mobile, and it is the same shortcut that quietly deflates a bounce rate vs exit rate count when the final event on a page never reaches the server.
document.visibilityState === "hidden" as the primary signal.Frequently Asked Questions
Is beforeunload still useful for anything?
Beforeunload is still useful for warning a user about unsaved changes through a browser-native confirmation dialog. MDN's own recommendation is to attach the listener only while unsaved changes exist and remove it once they are saved, instead of using it as a general page-exit signal.
Why does pagehide not block the back-forward cache the way beforeunload once did?
Pagehide does not block bfcache because it was designed as part of the Page Lifecycle API specifically to signal a page transition without the side effects unload and beforeunload carry. MDN states plainly that, unlike unload and beforeunload, a pagehide listener does not make a page ineligible for bfcache.
Should visibilitychange replace pagehide entirely?
Visibilitychange should be the primary signal, with pagehide kept as a fallback, per MDN's own guidance. Some browsers or contexts do not fire visibilitychange in every exit scenario, so pagehide catches the cases visibilitychange misses instead of replacing it outright.
What happens if an analytics call uses fetch instead of sendBeacon on page exit?
A plain fetch call started during page exit can be cancelled before the browser finishes sending it, since the page is already unloading, and the resulting AbortError is a side effect of the exit rather than a failed request. sendBeacon exists specifically to avoid that: the browser accepts the request and keeps trying to deliver it independent of whether the page has already gone away, up to its roughly 64 KiB limit.
Does the back-forward cache affect analytics at all?
The back-forward cache restores a page from memory on a back or forward navigation instead of reloading it, which means a page's JavaScript does not re-run and any pageview logic that depends on a fresh page load does not fire again. A pageshow event with event.persisted === true is the signal that a restore happened, which analytics logic needs to check for separately from a first load.
Why does Firefox treat beforeunload differently from other browsers for bfcache?
Firefox disqualifies a page from bfcache if it has a beforeunload listener attached, a stricter stance than browsers that stopped disqualifying beforeunload pages for bfcache after previously doing so. The safest choice across browsers is still to add a beforeunload listener only when unsaved changes exist, since that keeps the listener off the page during the navigations where bfcache eligibility matters most.
Flowsery
Start Your 14-Day Free Trial
Real-time dashboard
Goal tracking
Cookie-free tracking
What happens if an analytics payload is larger than sendBeacon's 64 KiB limit?
A sendBeacon call capped at roughly 64 KiB rejects a larger payload outright, so it never reaches the server. Switching to fetch with the keepalive option handles payloads above that size while still surviving a page that is already unloading.
Is the unload event a safer fallback than beforeunload?
Unload is worse, not safer. MDN calls it extremely unreliable on mobile, and it still blocks bfcache on desktop Chrome and Firefox, the exact cost pagehide was built to avoid. Treat it as legacy code to avoid rather than a fallback to reach for.
Does adding pagehide and visibilitychange listeners make a page heavier?
Flowsery ships its script under 10 KB specifically so this listener logic adds negligible weight to a page that's already trying to leave. The cost sits in the sendBeacon payload size, not in the event listeners themselves.
How does flushing on pagehide affect session timeout tracking?
Pagehide fires the moment a page hides behind another one in session history, which is also the moment a session timeout clock would otherwise keep running against a page nobody is looking at. Flushing there closes out the session at the right instant instead of letting idle time accumulate against a tab the user already left.
Was This Article Helpful?
Let us know what you think!
See us more often in Google
One click marks Flowsery as a preferred source, so our articles sit higher in your Top Stories, AI Mode, and AI Overviews.
Before you go...
Flowsery
Revenue-first analytics for your website
Track every visitor, source, and conversion in real time. Simple, powerful, and cookie-free.
Real-time dashboard
Goal tracking
Cookie-free tracking
Related Glossary Terms


Reading Entry Page vs Landing Page in GA4 and Adobe
The entry page vs landing page split matters because GA4 ships only a Landing page dimension and Adobe ships only Entry page, computed from different fields.


Why Unique Visitors Meaning Changes From Tool to Tool
The unique visitors meaning depends on the tool: GA4 estimates it, Matomo refuses to compute it on long ranges, Adobe dedupes it across the whole report.
Why sendBeacon vs fetch keepalive Is a 64 KiB Decision
Comparing sendBeacon vs fetch keepalive: both share one 64 KiB in-flight budget, but only sendBeacon locks your Content-Type and hides the server response.


What the Numbers Say About Average Bounce Rate by Industry
Nine tracked industries return a documented average bounce rate by industry ranging from 35.76% to 48.38%, sourced from Databox data dated September 2024.


How B2B and B2C Sites Compare on Average Session Duration Benchmarks
Databox's own data puts average session duration benchmarks at 77.61 seconds for B2B sites and 92.33 seconds for B2C sites, split by industry and device too.


What Digital Experience Analytics Covers That Web Analytics Misses
Unlike event counting, digital experience analytics reconstructs the visit itself, using session replay, heatmaps, friction detection and journey analysis.
Related Articles


What direct / none in GA4 Means and How It Differs From (not set)
Seeing direct / none in GA4 means a session arrived with no referrer and no campaign tag. How it differs from (not set), Unassigned and the Direct channel.


Why Direct Traffic Is the Bucket for Everything Analytics Could Not Attribute
Sessions land in direct traffic when no referrer and no campaign tag survive the hop. The real causes: referrer policies, apps, PDFs, redirects, QR codes.


How GA4 Data Thresholding Hides Rows From Your Reports
Google applies GA4 data thresholding to withhold demographic rows from a report. What triggers it, why the cutoff is undocumented, and what reduces it.

