loading...
Cover image for How to Detect Idle Browser Tabs with JavaScript

How to Detect Idle Browser Tabs with JavaScript

vorillaz profile image vorillaz ・3 min read

In some cases, we can found ourselves doing lots of intensive, CPU hungry tasks while the users are interacting with our end products or applications.
Firing pollers, establishing WebSocket connections or even loading media like videos or images could become performance dreadlocks especially if these tasks are consuming resources while there is no need to. It's a really good and meaningful practice to release the main thread from unnecessary workloads or network requests while the users are not actively interacting with the interface. In another manner, in an industry where most hosting providers are introducing quota-based pricing models reducing the network request could also reduce the costs for running your application or services.

The Page Visibility API

All the modern web browsers have incorporated the Page Visibility API which allows us to detect when a browser tab is hidden, moreover, we can also register an event listener in order to detect signals upon visibility changing.

document.visibilityState

The document.visibilityState could either be visible while the page is in a foreground
tab of a non-minimized window or hidden while the page is not actually visible to the user.

We can directly access the document.visibilityState as:

console.log(document.visibilityState);
// => It could be `visible` or `hidden`

visibilitychange Event

We also can easily detect changes in the visibility property using an event listener.

const onVisibilityChange = () => {
  if (document.visibilityState === 'hidden') {
    console.log('> The window is hidden.');
  } else {
    console.log('> The window is visible.');
  }
};
document.addEventListener('visibilitychange', onVisibilityChange, false);

An Example with Polling

Consider a scenario where we are polling our API for updates and we want to avoid making unnecessary calls for idle users. A simplified example would look like this:

const poll = () => {
  const interval = 1500;
  let _poller = null;
  const repeat = () => {
    console.log(`~ Polling: ${Date.now()}.`);
  };

  return {
    start: () => {
      _poller = setInterval(repeat, interval);
    },
    stop: () => {
      console.log('~ Poller stopped.');
      clearInterval(_poller);
    }
  };
};

const poller = poll();
poller.start();

const onVisibilityChange = () => {
  if (document.visibilityState === 'hidden') {
    poller.stop();
  } else {
    poller.start();
  }
};

document.addEventListener('visibilitychange', onVisibilityChange, false);

Asynchronously Loading in the Background

But sometimes we can accelerate our users' end experience by following the other way around. Instead of canceling all jobs and requests we can asynchronously load external dependencies or assets. In that way, users' end experience would be more "contentful" and rich when they come back.

Webpack

Using ES2015 dynamic imports proposal along with the appropriate Webpack configuration manifest we can easily load additional modules or assets in the background

let loaded = false;
const onVisibilityChange = () => {
  if (document.visibilityState === 'hidden') {
    // Aggresively preload external assets ans scripts
    if (loaded) {
      return;
    }
    Promise.all([
      import('./async.js'),
      import('./another-async.js'),
      import(/* webpackChunkName: "bar-module" */ 'modules/bar'),
      import(/* webpackPrefetch: 0 */ 'assets/images/foo.jpg')
    ]).then(() => {
      loaded = true;
    });
  }
};

document.addEventListener('visibilitychange', onVisibilityChange, false);

Rollup

Rollup does also support dynamic import out of the box.

let loaded = false;
const onVisibilityChange = () => {
  if (document.visibilityState === 'hidden') {
    // Aggresively preload external assets ans scripts
    if (loaded) {
      return;
    }
    Promise.all([
      import('./modules.js').then(({default: DefaultExport, NamedExport}) => {
        // do something with modules.
      })
    ]).then(() => {
      loaded = true;
    });
  }
};

document.addEventListener('visibilitychange', onVisibilityChange, false);

Preload with Javascript

Besides using a bundler we can also preload static assets like images using just a few lines of JavaScript.

let loaded = false;

const preloadImgs = (...imgs) => {
  const images = [];
  imgs.map(
    url =>
      new Promise((resolve, reject) => {
        images[i] = new Image();
        images[i].src = url;
        img.onload = () => resolve();
        img.onerror = () => reject();
      })
  );
};

const onVisibilityChange = () => {
  if (document.visibilityState === 'hidden') {
    // Aggresively preload external assets ans scripts
    if (loaded) {
      return;
    }
    Promise.all(
      preloadImgs(
        'https://example.com/foo.jpg',
        'https://example.com/qux.jpg',
        'https://example.com/bar.jpg'
      )
    )
      .then(() => {
        loaded = true;
      })
      .catch(() => {
        console.log('> Snap.');
      });
  }
};

document.addEventListener('visibilitychange', onVisibilityChange, false);

Micro-interactions

Finally, a neat approach for grabbing users' attention is dynamically changing the favicon, using just a few pixels you can retain interaction.

const onVisibilityChange = () => {
  const favicon = document.querySelector('[rel="shortcut icon"]');
  if (document.visibilityState === 'hidden') {
    favicon.href = '/come-back.png';
  } else {
    favicon.href = '/example.png';
  }
};

document.addEventListener('visibilitychange', onVisibilityChange, false);

References

You can also find this post on vorillaz.com

Posted on by:

vorillaz profile

vorillaz

@vorillaz

I can write JavaScript for robots.

Discussion

pic
Editor guide
 

I've used this API to detect if a blur event on an input was due to tabbing and prevent showing an invalidation message if the input was left in an invalid state since tabbing back will give it back it's focus.

 

That’s a great example and use case of the API. Thanks a lot for the reply John.

 

Thanks for this article. I would like to propose a little fix.

Try to do next:

  • Add your Example with Polling on the page.
  • Reload the page and switch a browser tab before your sample will be reloaded.
  • Wait until your page with sample will be reloaded.
  • Go back to your page.
  • You will see that poller calls console.log twice as often.
  • Try to stop the poller. It will work even after poller.stop() call.

This is due to double call of the poller.start. For the first time from your sample code. The second time you call it from the onVisibilityChange. There was no poller.stop method call between double poller.start method call in this case.

You can avoid it by calling the poller.stop method before the poller.start in the onVisibilityChange.