Lighthouse audit deprecations · Best Practices

"Unload event listeners are deprecated and will be removed": how to fix it

Quick answer: Chrome no longer fires the unload event by default, so Lighthouse lists any unload listener under "Uses deprecated APIs" and the console prints "Permissions policy violation: unload is not allowed in this document". Fix it by replacing unload with pagehide for cleanup and visibilitychange for saving state, sending analytics with sendBeacon or fetch keepalive, and upgrading old jQuery and vendor scripts that register the listener for you.
View raw .md for LLMs / your notes

This row appears under the Lighthouse audit Uses deprecated APIs when a script on the page registers a listener for the unload event. Chrome deprecated the event because it blocks the back/forward cache and never fired reliably on mobile, and in 2026 it finished turning the event off by default. The same problem shows up in the Chrome console as "[Violation] Permissions policy violation: unload is not allowed in this document." In our study of 269 Shopify storefronts, 148 of them (55.0%) failed the deprecations audit. This page covers what the two messages mean, how to find the script that registers the listener, what to use instead, and what to do when the listener belongs to a vendor.

TL;DR

What do the two messages mean?

Where you see itExact textWhat triggered it
Lighthouse, Best Practices, "Uses deprecated APIs"Unload event listeners are deprecated and will be removed.A script registered an unload listener during the audit. The Source column gives the file and line.
Chrome console[Violation] Permissions policy violation: unload is not allowed in this document.A script registered an unload listener in a document whose permissions policy blocks unload, which is now Chrome's default. The listener is accepted but will not fire.
DevTools, Application, Back/forward cacheUnload handler under ActionableThe page cannot enter the back/forward cache because of the listener.

All three point at the same line of code. The deprecations audit is binary: one row fails it, and there is no threshold. It carries a small weight in the Best Practices score and does not touch Performance.

Why did Chrome deprecate the unload event?

Two reasons, both documented in Chrome's deprecation plan:

  1. It blocks the back/forward cache. The cache keeps a page alive in memory when the user navigates away, so Back and Forward restore it instantly. A page with an unload listener cannot be kept alive, because the browser would have to run the handler at an unknown later time. Every page with the listener pays a full reload on Back.
  2. It never fired reliably. Mobile browsers kill background tabs without running unload, and desktop browsers skip it in several navigation paths. Code that depended on it, such as "send the final analytics ping on unload", silently lost data for years.

Chrome rolled the change out through a permissions policy. The unload policy's default moved from allow to deny: first for sites that opted in (Chrome 115, 2023), then for the top 50 sites (Chrome 135 to 142, 2025), then for all origins in steps from 1% of page loads in Chrome 146 (March 2026) to 100% in Chrome 155 (October 2026). Today the event does not fire in Chrome on desktop or Android unless the site opts back in. Chrome on iOS uses WebKit and still fires it.

How do I find the script that registers the unload listener?

  1. Read the Source column in the Lighthouse row. It gives the URL and line. A file on your domain is yours. A minified vendor file at line 1 is a third party.
  2. Search your own code for these patterns:
addEventListener('unload'
addEventListener("unload"
window.onunload
onunload=
$(window).unload(
$(window).on('unload'
jQuery(window).unload(
  1. Check jQuery. jQuery 1.x and 2.x register an unload listener internally to clean up event data in old Internet Explorer. We saw this in a live PageSpeed run: the row pointed at jquery/1.12.4/jquery.min.js, line 1. jQuery 3 removed that code, and it also removed the .unload() shorthand.
  2. Check vendor scripts. Older analytics SDKs, session recorders, chat widgets, A/B testing tools, and ad scripts registered unload listeners to flush their queues. The Source URL names the vendor.
  3. Confirm in DevTools. Open the Application panel, choose Back/forward cache, and click Test back/forward cache. An unload handler appears under Actionable. The same reason is exposed to scripts as unload-listener in performance.getEntriesByType('navigation')[0].notRestoredReasons.

What should I use instead of unload?

1. pagehide for cleanup

pagehide fires when the user navigates away or closes the tab normally, including when the page enters the back/forward cache, and it does not block the cache. It does not fire when the browser discards a background tab or kills its process, which is common on mobile. event.persisted is true when the page is being cached, so you can skip teardown that a restored page would need.

// Before: never fires in current Chrome, blocks bfcache everywhere.
window.addEventListener('unload', () => {
  socket.close();
});

// After: fires on normal navigation, lets the page be cached.
window.addEventListener('pagehide', (event) => {
  socket.close();
  if (event.persisted) {
    // The page may come back from the cache; reconnect in pageshow.
  }
});
window.addEventListener('pageshow', (event) => {
  if (event.persisted) socket = connect();
});

2. visibilitychange for saving state and sending analytics

The moment the document becomes hidden is the last state change a page can reliably observe. After it, the browser can discard the tab or kill its process without firing any more events, so even this handler is not guaranteed to run on every exit. Send the final analytics event here, and use a transport that survives the page going away. For data you cannot lose, such as drafts, save as the data changes (on input, debounced) and treat the hidden handler as a last flush, not the only save.

document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    localStorage.setItem('draft', editor.value);
    navigator.sendBeacon('/analytics', JSON.stringify({ event: 'session_end' }));
    // or: fetch('/analytics', { method: 'POST', body, keepalive: true });
  }
});

sendBeacon and fetch with keepalive: true let the request finish after the page is gone. A plain fetch or XMLHttpRequest in an unload path is cancelled.

3. beforeunload only for unsaved changes

beforeunload also blocks the back/forward cache while it is registered. Add it only when the page has unsaved changes, and remove it as soon as the changes are saved. Never use it for analytics or cleanup.

function onBeforeUnload(e) { e.preventDefault(); }
editor.addEventListener('input', () => window.addEventListener('beforeunload', onBeforeUnload));
form.addEventListener('submit', () => window.removeEventListener('beforeunload', onBeforeUnload));

Can I turn the unload event back on?

Yes, for your own document, with a response header. Chrome keeps this opt-in for the foreseeable future, but it does not make the deprecation row go away and it keeps your page out of the back/forward cache.

Permissions-Policy: unload=self

Allow it inside a cross-origin iframe as well:

Permissions-Policy: unload=(self "https://embed.example.com")
<iframe src="https://embed.example.com/widget" allow="unload"></iframe>

Use the header as a bridge while you migrate a large codebase, not as the fix. Managed Chrome deployments can also keep unload on for staff with the ForcePermissionPolicyUnloadDefaultEnabled enterprise policy, which changes nothing for your public visitors.

How do I fix it in Shopify, WordPress, or React?

Can I fix an unload listener in a third-party script?

You cannot edit vendor code. Your options:

  1. Update the SDK. Most vendors replaced unload with pagehide and visibilitychange in 2023 and 2024. The current version of the script is the fix.
  2. Load it later. The listener still registers, so this does not clear the row, but it keeps the vendor's other work off the critical path.
  3. Report it. Send the vendor the file URL and line from the Lighthouse row.
  4. Remove the tag if it does not earn its place. This is the only fix you fully control.

Do not add Permissions-Policy: unload=self to make a vendor's handler run again. It costs you the back/forward cache on every page and the handler still does not run for mobile visitors.

How do I verify the fix?

  1. Re-run Lighthouse. "Avoids deprecated APIs" should appear in the passed audits, unless another deprecation row remains.
  2. Reload with the console open. The "Permissions policy violation: unload is not allowed in this document" message should be gone.
  3. Run Test back/forward cache in the DevTools Application panel. The result should be "Successfully served from back/forward cache", with no unload handler under Actionable.
  4. Navigate away and press Back. The page should restore instantly, with scroll position and form state intact.

What mistakes should I avoid?


Audit your URL at https://lighthouse-md.com.

Audit your page now

Paste your URL, get scores plus a CLAUDE.md plan for Claude Code.

Run audit →
Add us as a preferred source on Google

One click, and Google shows more of lighthouse-md.com in your search results.