How to Reliably Send HTTP Requests Before Closing Browser Tabs?

We often encounter a classic scenario: the user is about to close the page or browser tab, and we need to seize this last opportunity to send some important information to the server.

However, this seemingly simple requirement is fraught with challenges in practice. Traditional asynchronous requests (such as <span>fetch</span> or <span>XMLHttpRequest</span>) are highly likely to be interrupted by the browser during the page unload event, leading to request failures.

The Root of the Problem: Why Do Regular Requests Fail?

When a user closes a tab, the browser triggers a series of page unload events, such as <span>pagehide</span> and <span>unload</span>.

During this process, any standard asynchronous <span>fetch</span> or <span>XMLHttpRequest</span> request initiated in the <span>unload</span> event handler faces a problem: the request is just sent, but the page has already been destroyed.

Since the JavaScript execution environment of the page no longer exists, the browser is not obliged to complete this request and will actively cancel it.

In the past, developers would use synchronous <span>XMLHttpRequest</span> to solve this problem. It forces the main thread to block until the request is complete. Although this method is “effective,” it is devastating to user experience—it causes the browser UI to freeze, and the page becomes unresponsive until the network request is finished.

So, how can we reliably send this “last letter” without compromising user experience?

Modern Solution One: <span>navigator.sendBeacon()</span>

<span>navigator.sendBeacon()</span> is an API specifically designed by W3C to address such issues. Its core mission is to: reliably send a small amount of data to the server in an asynchronous, non-blocking manner.

How It Works

When we call <span>sendBeacon()</span>, the browser adds this request to an internal queue and immediately returns, without blocking the page unload. The browser guarantees to send this request at an appropriate time (e.g., in the background), even if the page that initiated the request has already closed.

Features

  • High Reliability: Guaranteed by the browser to send, unaffected by page unload
  • Asynchronous and Non-blocking: Does not affect the speed and experience of the user closing the page
  • Easy to Use: The API is very intuitive
  • Data Limitations: Can only send POST requests unidirectionally and cannot customize request headers

Code Example

Suppose we need to send an analysis log containing the time spent on the page when the user leaves.

// It is recommended to use the 'pagehide' event, which is more reliable than 'unload'
window.addEventListener('pagehide', (event) => {
 // event.persisted being true indicates the page has entered the back-forward cache (bfcache) and has not actually unloaded
 // In this case, we usually do not send a beacon
 if (event.persisted) {
    return;
  }

 const analyticsData = {
    timeOnPage: Math.round(performance.now()),
    lastAction: 'close_tab',
  };

 // Convert data to Blob, which is one of the formats supported by sendBeacon
 const blob = new Blob([JSON.stringify(analyticsData)], {
    type: 'application/json; charset=UTF-8',
  });

 // Use sendBeacon to send data
 // This method will return true (successfully added to the queue) or false (data too large or format error)
 const success = navigator.sendBeacon('/log-analytics', blob);

 if (success) {
    console.log('Analysis log has been successfully added to the sending queue.');
  } else {
    console.error('Unable to send analysis log.');
  }
});

Modern Solution Two: <span>fetch()</span> with <span>keepalive: true</span>

<span>fetch</span> API, as the cornerstone of modern network requests, also provides an elegant solution. By setting <span>keepalive: true</span> in the <span>fetch</span> ‘s <span>init</span> object, we can tell the browser: “This request is important, please continue to complete it after the page unloads.”

How It Works

<span>fetch({ keepalive: true })</span> works similarly to <span>sendBeacon</span>. It marks a <span>fetch</span> request as “keep-alive,” allowing its lifecycle to extend beyond the current page. The browser will handle it in the background, just like it does with <span>sendBeacon</span> requests.

Features

  • High Flexibility: Compared to <span>sendBeacon</span>, it supports more HTTP methods (such as <span>POST</span>, <span>PUT</span>, etc.) and allows limited request header customization
  • Unified API: If we have already used <span>fetch</span> extensively in our project, using <span>keepalive</span> can maintain consistent code style
  • Cannot Handle Responses Either: Like <span>sendBeacon</span>, since the page is already closed, we cannot read or process the server’s response on the frontend

Code Example

Suppose we need to automatically save the draft in a text editor when the user closes the page. Using a <span>PUT</span> request may be more in line with RESTful style.

window.addEventListener('pagehide', (event) => {
 if (event.persisted) {
    return;
  }

 const draftContent = document.getElementById('editor').value;
 if (!draftContent) return;

 const draftData = {
    content: draftContent,
    timestamp: Date.now(),
  };

 // Use fetch and keepalive: true
 // Note: Even if the request is successful, the .then and .catch here may not execute because the page is unloading
 try {
    fetch('/api/drafts/save', {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(draftData),
      // This is key!
      keepalive: true,
    });
    console.log('Draft save request has been submitted.');
  } catch (e) {
    // This catch block is unlikely to catch network errors
    console.error('An error occurred while submitting the draft save request:', e);
  }
});

How to Choose?

If our requirement is to send simple analytics or log data, <span>navigator.sendBeacon()</span> is the most straightforward and semantically appropriate choice. If we need greater flexibility, such as using the <span>PUT</span> method to update resources or needing to set specific request headers, <span>fetch({ keepalive: true })</span> is the better choice.

With these two modern APIs, we can ensure the successful delivery of critical data without sacrificing user experience.

Leave a Comment