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
- Page Visibility in W3C
- Document.visibilityState in MDN
- Document API: visibilityState browser support
- ES2015 dynamic imports with Webpack -Dynamic imports with Rollup
You can also find this post on vorillaz.com
Top comments (4)
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:
Example with Polling
on the page.poller
callsconsole.log
twice as often.poller
. It will work even afterpoller.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 theonVisibilityChange
. There was nopoller.stop
method call between doublepoller.start
method call in this case.You can avoid it by calling the
poller.stop
method before thepoller.start
in theonVisibilityChange
.Nice article !