In some cases, when users interact with our end product or application, we find ourselves performing many intensive, CPU-heavy tasks. Starting a poller, setting up a WebSocket connection, and even loading media such as video or images can be a performance hurdle, especially if these tasks consume resources when they are not needed. Releasing the main thread from unnecessary workloads or network requests while the user is not actively interacting with the interface is a good and meaningful practice. On the other hand, in an industry where most hosting providers are introducing quota-based pricing, reducing network requests can also reduce the cost of running applications or services.

Page Visibility API

All modern web browsers include a page visibility API that allows us to detect when browser tabs are hidden, and we can register an event listener to detect changes in visibility.

document.visibilityState

When the page is in the foreground, document.visibilityState may be visible, minimizing the window’s “tabs” or hiding.

We can access document.visibilityState directly by:

console.log(document.visibilityState);
// => It can be "visible" or "hidden"
Copy the code

visibilitychange Event

We can also easily detect changes in visibility properties using event listeners.

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

Polling sample

Consider a case where we are polling the API for updates and want to avoid unnecessary calls to idle users. A simplified example is shown below:

const poll = () = > {
  const interval = 1500;
  let _poller = null;
  const repeat = () = > {
    console.log(`~ Polling: The ${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);
Copy the code

Load asynchronously in the background

But sometimes we can speed up the user’s end experience by going the other way. Instead of canceling all jobs and requests, we can asynchronously load external dependencies or assets. This way, when the user comes back, their final experience will be “enriched” and richer.

Webpack

Using ES2015 dynamic import recommendations and appropriate Webpack configuration listings, 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);
Copy the code

Rollup

Rollup also supports dynamic imports 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);
Copy the code

Preload with Javascript

In addition to using a binder, we can preload static resources (such as images) with 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);
Copy the code

The micro interaction

Finally, a clever way to grab the user’s attention is to change the icon dynamically, using only a few pixels to keep the interaction going.

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);
Copy the code

The resources

  • Page Visibility in W3C
  • Document.visibilityState in MDN
  • Document API: visibilityState browser support
  • ES2015 dynamic imports with Webpack -Dynamic imports with Rollup

Source: Dev.to, author: Vorillaz, translation: Front-end Full Stack Developer