circuit

How to Detect the Closing of a Browser Tab




Photo by Remotar Jobs on Unsplash

Sometimes, you need to do something right before the user closes a tab. It could be clearing cookies or sending an API call.

In JavaScript/TypeScript, however, there is no direct/standard way to identify whether the user has closed a tab and/or the browser window. I was surprised to find that there is no straightforward way to do so. In this article, you will learn a way to detect page reload, tab close and browser close actions effectively and also distinguish between them.

The examples I will be showing below are using Angular 10. If you are using another framework or JavaScript in general, the syntax would vary but the theory remains the same.

beforeunload

JavaScript provides an event handler called beforeunload. This event is called whenever the resources are removed/unloaded from the client.

This means this event will be triggered whenever the user either reloads the page, closes the tab or the browser window. It is enough if you are looking for just that. However, for most of the applications, we need to distinguish between a 'close' (tab or window) and a 'reload'. Let's see how we can do that.

// Angular Example for beforeunload event handler
@HostListener('window:beforeunload', ['$event'])
beforeunloadHandler(event): void {
  // Do something
}

1. Identifying a Page Reload

We know so far, a page reload would also trigger beforeunload event. When the event is triggered we do not know what triggered it (reload, tab close or browser close). Within that event handler, we need to identify whether the page was reloaded or not.

Page reload can be identified using NavigationTiming API. You can read all about it here.

Please note that window.performance.navigation is deprecated and hence, you should avoid using that. Use the properties provided by the NavigationTiming API instead.

Based on the documentation of NavigationTiming, we need to look for navigation type 'reload' within the array of navigation entry type. We will avoid using loops for simplicity. Let's code that

let pageReloaded = window.performance
                 .getEntriesByType('navigation')
                 .map((nav) => (nav as any).type)
                 .includes('reload');

Just like that, now pageReloaded carries the boolean value of whether the page is reloaded or not. You would want to put it within our beforeunload event handler.

NOTE: There are many reasons why we didn't use loops. I went into good detail about why you shouldn't use loops in this article. In case you are using JavaScript and not TypeScript, the only change would be the use of the arrow function in map.

2. Detecting Tab Getting Closed

Unlike page reload, there is no standard way to detect whether the tab was closed or not. However, we can make use of local storage to achieve this.

Local Storage in the browser is shared by the same domain. This means you can have multiple tabs open in the browser from let's say www.google.com and they all share the same local storage.

The idea is simple, we can keep count of the total number of tabs open in the browser for a specific domain. Instantiated at 1, it will increase by 1 every time a new tab is opened (a new page is instantiated) and decrease by 1 when a tab is closed (using beforeunload event handler).

NOTE: For the tab getting closed, we need to make sure we don't decrement the tab count when the page is reloaded. We have seen in point 1 how we can detect a page reload.

Let's take a look at the code

2.1. Incrementing the tab count

ngOnInit() {
  // We need to parse into integer since local storage can only
  // store strings.
  let tabCount = parseInt(localStorage.getItem("windowCount"));

  // Then we instantiate tabCount if it doesn't already exist
  // OR Increment by 1 if it already exists
  tabCount = Number.isNaN(tabCount) ? 1 : ++tabCount;

  // Set the count on local storage
  localStorage.setItem('tabCount', tabCount.toString());
}

2.2. Decrementing the tab count

@HostListener('window:beforeunload', ['$event'])
beforeunloadHandler(event): void {
  if(!pageReloaded) { // The pageReloaded boolean we set earlier
    let tabCount = parseInt(localStorage.getItem('tabCount'));
    --tabCount;
    localStorage.setItem('tabCount', tabCount.toString());
  }
}

Just like this, we can now tell when a tab is getting closed. Every time our tabCount decreases by 1 a tab has been closed. You can do whatever you want now on detecting this.

There is another way to get all opened tabs in the browser. Using window.getAll() we can achieve the exact thing plus more. However, it is not supported by latest Firefox and Safari versions. It also requires permission from the user. For our purposes, using the local storage method is more apt(since window.getAll() is asynchronous) and it keeps things simple.

Now let's take it one step further. We will now detect whether it was a browser close or a tab close.

NOTE: If all you wanted was to detect if a tab or the browser was closed and not distinguish between them, you don't even need to keep a tabCount. If you hit the beforeunload event handler and the pageReloaded boolean is false, it is confirmed that the tab or window was closed.

3. Detecting a Browser Close

To understand a browser close, we need to first understand how a browser is closed.

When a user closes the browser window, each tab within that browser is closed one after the other (based on my experiments, the delay in Chrome is about 1 ms on light to average load..

Now, we know that a browser close will trigger beforeunload event handler. Since each tab will be closed one after the other, we can try to catch the delay between two consecutive tab close. If the closure delay is minute(1ms-50ms), that's our signal that the browser is closing.

isBrowserClosed() {
  var localStorageTime = parseInt(localStorage.getItem('storageTime'));
  var currentTime = new Date().getTime();
  var timeDifference = currentTime - localStorageTime;

  if (timeDifference < 50) {
    //Browser is being closed
    // Do something before browser closes.
  }
}

NOTE: To detect browser close accurately, we need to update storageTime if the difference between the current time and the storage time onbeforeunload is too high. The code below is an example of that.

@HostListener('window:beforeunload', ['$event'])
beforeunloadHandler(event): void {
  if(!pageReloaded) { // The pageReloaded boolean we set earlier
    if (storageTime === null || storageTime === undefined
        || (storageTime - new Date().getTime()) > 1000) {
      // If storageTime is null, undefined or is older than 1 second
      // we update the storage time.
    }
  }
}

This little trick also reduces the probability that we will miss the browser close event due to the client's poor hardware performance.

In case of really poor client hardware performance, the worst-case scenario is that every beforeunload event would be detected as a tab close.

Conclusion

Based on what we saw so far, we can boil our detection method to 3 points:

  1. If navigation entries have reload navigation type, the user reloaded the page.

  2. If the tab count is decreased, the user closed a tab.

  3. If the tab count is decreased AND the storage time difference is less than 50ms, the user probably closed the browser.

We say probably in the third point because our bottleneck is the client's hardware performance. Even though we have placed extra measures to ensure accurate results, there is still a possibility that we might miss it. That being said, in most cases, this would suffice.

I hope you enjoyed this read.

My name is Vishnu Sasidharan and I write technical articles, code, and tell stories. I aim to reduce complex concepts into something simple and explain it in layman's terms. I also share real-life stories that have inspired me.




Continue Learning