JavaScript, browsers, window close - send an AJAX request or run a script on window closing

60,390

Solution 1

There are unload and beforeunload javascript events, but these are not reliable for an Ajax request (it is not guaranteed that a request initiated in one of these events will reach the server).

Therefore, doing this is highly not recommended, and you should look for an alternative.

If you definitely need this, consider a "ping"-style solution. Send a request every minute basically telling the server "I'm still here". Then, if the server doesn't receive such a request for more than two minutes (you have to take into account latencies etc.), you consider the client offline.


Another solution would be to use unload or beforeunload to do a Sjax request (Synchronous JavaScript And XML), but this is completely not recommended. Doing this will basically freeze the user's browser until the request is complete, which they will not like (even if the request takes little time).

Solution 2

Updated 2021

TL;DR

Beacon API is the solution to this issue (on almost every browser).

A beacon request is supposed to complete even if the user exits the page.

When should you trigger your Beacon request ?

This will depend on your usecase. If you are looking to catch any user exit, visibilitychange (not unload) is the last event reliably observable by developers in modern browsers.

NB: As long as implementation of visibilitychange is not consistent across browsers, you can detect it via the lifecycle.js library.

# lifecycle.js (1K) for cross-browser compatibility
# https://github.com/GoogleChromeLabs/page-lifecycle

<script defer src="/path/to/lifecycle.js"></script>
<script defer>
lifecycle.addEventListener('statechange', function(event) {

  if (event.originalEvent == 'visibilitychange' && event.newState == 'hidden') {
    var url = "https://example.com/foo";
    var data = "bar";

    navigator.sendBeacon(url, data);
  }
});
</script>

Details

Beacon requests are supposed to run to completion even if the user leaves the page - switches to another app, etc - without blocking user workflow.

Under the hood, it sends a POST request along with the user credentials (cookies), subject to CORS restrictions.

    var url = "https://example.com/foo";
    var data = "bar";

    navigator.sendBeacon(url, data);

The question is when to send your Beacon request. Especially if you want to wait until the last moment to send session info, app state, analytics, etc.

It used to be common practice to send it during the unload event, but changes to page lifecycle management - driven by mobile UX - killed this approach. Today, most mobile workflows (switching to new tab, switching to the homescreen, switching to another app...) do not trigger the unload event.

If you want to do things when a user exits your app/page, it is now recommended to use the visibilitychange event and check for transitioning from passive to hidden state.

document.addEventListener('visibilitychange', function() {
      
  if (document.visibilityState == 'hidden') {
    
     // send beacon request
  }

});

The transition to hidden is often the last state change that's reliably observable by developers (this is especially true on mobile, as users can close tabs or the browser app itself, and the beforeunload, pagehide, and unload events are not fired in those cases).

This means you should treat the hidden state as the likely end to the user's session. In other words, persist any unsaved application state and send any unsent analytics data.

Details of the Page lifecyle API are explained in this article.

However, implementation of the visibilitychange event, as well as the Page lifecycle API is not consistent across browsers.

Until browser implementation catches up, using the lifecycle.js library and page lifecycle best practices seems like a good solution.

# lifecycle.js (1K) for cross-browser compatibility
# https://github.com/GoogleChromeLabs/page-lifecycle

<script defer src="/path/to/lifecycle.js"></script>
<script defer>
lifecycle.addEventListener('statechange', function(event) {

  if (event.originalEvent == 'visibilitychange' && event.newState == 'hidden') {
    var url = "https://example.com/foo";
    var data = "bar";

    navigator.sendBeacon(url, data);
  }
});
</script>

For more numbers about the reliability of vanilla page lifecycle events (without lifecycle.js), there is also this study.

Adblockers

Adblockers seem to have options that block sendBeacon requests.

Cross site requests

Beacon requests are POST requests that include cookies and are subject to CORS spec. More info.

Solution 3

1) If you're looking for a way to work in all browsers, then the safest way is to send a synchronous AJAX to the server. It is is not a good method, but at least make sure that you are not sending too much of data to the server, and the server is fast.

2) You can also use an asynchronous AJAX request, and use ignore_user_abort function on the server (if you're using PHP). However ignore_user_abort depends a lot on server configuration. Make sure you test it well.

3) For modern browsers you should not send an AJAX request. You should use the new navigator.sendBeacon method to send data to the server asynchronously, and without blocking the loading of the next page. Since you're wanting to send data to server before user moves out of the page, you can use this method in a unload event handler.

$(window).on('unload', function() {
    var fd = new FormData();
    fd.append('ajax_data', 22);
    navigator.sendBeacon('ajax.php', fd);
});

There also seems to be a polyfill for sendBeacon. It resorts to sending a synchronous AJAX if method is not natively available.

IMPORTANT FOR MOBILE DEVICES : Please note that unload event handler is not guaranteed to be fired for mobiles. But the visibilitychange event is guaranteed to be fired. So for mobile devices, your data collection code may need a bit of tweaking.

You may refer to my blog article for the code implementation of all the 3 ways.

Solution 4

I also wanted to achieve the same functionality & came across this answer from Felix(it is not guaranteed that a request initiated in one of these events will reach the server).

To make the request reach to the server we tried below code:-

onbeforeunload = function() {

    //Your code goes here.

    return "";
} 

We are using IE browser & now when user closes the browser then he gets the confirmation dialogue because of return ""; & waits for user's confirmation & this waiting time makes the request to reach the server.

Solution 5

Years after posting the question I made a way better implementation including nodejs and socket.io (https://socket.io) (you can use any kind of socket for that matter but that was my personal choice).

Basically I open up a connection with the client, and when it hangs up I just save data / do whatever I need. Obviously this cannot be use to show anything / redirect the client (since you are doing it server side), but is what I actually needed back then.

 io.on('connection', function(socket){
      socket.on('disconnect', function(){
          // Do stuff here
      });
 });

So... nowadays I think this would be a better (although harder to implement because you need node, socket, etc., but is not that hard; should take like 30 min or so if you do it first time) approach than the unload version.

Share:
60,390
zozo
Author by

zozo

Same old me.

Updated on July 09, 2022

Comments

  • zozo
    zozo almost 2 years

    I'm trying to find out when a user left a specified page. There is no problem finding out when he used a link inside the page to navigate away but I kind of need to mark up something like when he closed the window or typed another URL and pressed enter. The second one is not so important but the first one is. So here is the question:

    How can I see when a user closed my page (capture window.close event), and then... doesn't really matter (I need to send an AJAX request, but if I can get it to run an alert, I can do the rest).

  • Jonathon
    Jonathon almost 13 years
    hmm (-1), I would be very interested to know what is wrong with this method. Never used it myself but have encountered it many times as the solution to your exact problem.
  • Felix
    Felix almost 13 years
    I have posted an answer in which I explain the -1.
  • Jonathon
    Jonathon almost 13 years
    Well if he is doing a logout then I have seen working examples where the unload opens a popup windows that then logs the user out (doe this solve the non guarentee issue?).
  • Felix
    Felix almost 13 years
    No. I bet there are popup blockers out there that do not allow popups on those events. Also, it's terrible UX to open a popup when the user closes the tab/window.
  • Jonathon
    Jonathon almost 13 years
    I assume the issue is that the request takes time and the browser might be finished with unload by the time it is finished (so it just might never finish)?
  • zozo
    zozo almost 13 years
    I am actually doing the I am still here thing. But is kind of slow and reduce user experience. Also reducing the interval of the "i'm still here" can kill the server at a high rate of users. So I was trying to replace it with something else. When you say the request is not reliable how unreliable u mean? I can use that and double it with sending messages at a higher interval is has more than 50% rate of success. I think that can improve performance on both sides (server and client).
  • Felix
    Felix almost 13 years
    Basically, yes. When closing a tab/window the browser usually clears everything related to it, which often means cancelling any AJAX requests.
  • Jonathon
    Jonathon almost 13 years
    Well then you might want to look into the opening a simple small popup that then logs you out. I agree with Felix, it did not work perfectly but most popup blockers allow you to set exceptions easily and overall it works pretty well when I used it. It is not a perfect solution, but then nothing ever is. note: You will want to store the code for the popup with the mother page and not send a request for it from the server or you will just encounter the same problem.
  • Jonathon
    Jonathon almost 13 years
    To answer the % of success, I imagine it would be totally dependant on the spend of the request (so internet/computer speed/server speed might all come into play).
  • zozo
    zozo almost 13 years
    Ok... how about this. Would the "closing" stop if I use a prompt? Some thing like onUnload/beforUnload use a prompt with "are u sure u want to leave?" This should give the page suficient time to send the request... Or if not I can make a pop up like u suggested and mime a prompt in it.
  • Felix
    Felix almost 13 years
    I think it would be your best bet (although you'd still have to do the pinging if you want to be 100% sure). BTW, the way to do this is to return "a string"; in your beforeunload handler (doing a confirm() won't work).
  • Michal Politzer
    Michal Politzer about 7 years
    it seems current Chrome ignores this at all if there is more than only retrun string statement in beforeunload function. So you can do return "Do you want to close?", but you cannot do anything else…
  • Greg
    Greg over 6 years
    The time spent on your for loop will vary based on CPU, JS Engine and other factors. A slightly less bad idea would be to block the event loop for 100ms or any other value that is deemed long enough after experimenting
  • Baishu
    Baishu over 6 years
    In 2018, this is the best response. Use the beacon API. Read the blog article mentionned in the answer ( usefulangle.com/post/62/… ) for more details.
  • Baishu
    Baishu over 6 years
    2018 update: beacon API landed in browsers and solves this problem. See this answer : stackoverflow.com/a/47108290/2569457
  • zozo
    zozo over 4 years
    Hello and welcome to SO. Not downvoting (since you are new :) ) but your answer brings nothing new to the accepted answer. Also if I am reading it correctly it triggers the ConfirmLeave function everytime user moves mouse outside the window, resulting in fake requests (for example each time he moves mouse over task bar or something). Also it doesn't treat any kind of touch / mobile cases (since event is bound only on mouseOut, removed on over, and never bound back). Also consider the case of simply pressing f5 (not working either in your example).
  • zozo
    zozo over 4 years
    In 2011 (when I posted the question) it was not possible (or at least not so simple as today), but nowadays you can use a websocket for this, which basically brings you the advantages of the ping style solution without their drawbacks. If you wanna check something about that I would gladly upvote an answer like it (I was considering posting one myself but... got lazy :) ).
  • Alfie Robles
    Alfie Robles over 4 years
    This answer changed my life! Thanks a lot!
  • godblessstrawberry
    godblessstrawberry over 3 years
    MDN says unload and beforeunload aren’t the right events to use with sendBeacon. Instead, use visibilitychange so this is useless developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeaco‌​n
  • Baishu
    Baishu over 3 years
    @godblessstrawberry Indeed. MDN reverted their recommendation for using unload recently : github.com/mdn/sprints/issues/3722 . I have rewritten the answer to take this into account.
  • Baishu
    Baishu over 3 years
    @AlfieRobles please check the changes to this answer if you are relying on the unload event for important data.
  • Lennart Rolland
    Lennart Rolland almost 3 years
    I knew it existed but couldn't remember it's name. This is the correct answer IMO "navigator.sendBeacon" <--
  • Greg Venech
    Greg Venech over 2 years
    This looks promising but it seems we can't set request headers with Beacon requests? The MDN page for sendBeacon doesn't mention them but I found this article which notes that only Content-Type is possible. Am I wrong on this or will this likely require the receiving API to be updated so as to allow bypassing things like authentication (or handling them via the request payload rather than headers)?
  • Greg Venech
    Greg Venech over 2 years
    I'm also wondering if keep-alive, suggested by that article, would play a useful role here at all.
  • Bharadwaj Giridhar
    Bharadwaj Giridhar over 2 years
    Note: Adblock seems to block most of the calls made using navigator.sendBeacon();
  • Baishu
    Baishu over 2 years
    @GregVenech About keep-alive, your link mentions : "Connection-specific header fields such as Connection and Keep-Alive are prohibited in HTTP/2. Chrome and Firefox ignore them in HTTP/2 responses, Safari does not load any response that contains them."
  • Baishu
    Baishu over 2 years
    @GregVenech About authentication, my understanding is that sendBeacon, which makes sense as a background event, will embark cookies like other requests, enabling your app to identify the user
  • Baishu
    Baishu over 2 years
    @BharadwajGiridhar can you point to info on that ? I have found this for adblockplus. Is that it ?
  • Bharadwaj Giridhar
    Bharadwaj Giridhar over 2 years
    @Baishu don't have a link, but I tested with the same adblockplus.org plugin on Edge (Based on Chromium) and saw by navigator.sendBeacon() calls being blocked. In my use case, I run crewcharge.com where my script (like google analytics) needs to be embedded onto another site. Maybe adblock just blocks navigator.sendBeacons () from cross-origin sites. Please test accordingly.