Using the PageVisibility API

HTML5 Rocks

Introduction

As Web developers, we tend to get excited by new technologies that enable us to create ever more engaging, interactive Web pages. 3D graphics with WebGL? Absolutely. Advanced audio capabilities with WebAudio? Sure thing. Real-time collaboration applications using the Web camera and microphone? Sign me up! Less exciting, though equally important, are the technologies that allow us to build applications that run more efficiently and provide a better overall user experience. This is where an API like PageVisibility comes in. The Page Visibility API performs a simple but important function – it lets your application know when a page is visible to the user. This basic piece of information enables the creation of Web pages that behave differently when they are not being viewed. Consider a few examples:

  • A Web page that retrieves information from a server can slow down its update cycle when not being actively viewed
  • A page that displays a rotating image carousel or video/audio content can pause until the user displays the page again
  • An application can decide to display notifications to the user only when it is hidden from view

At first, this API may not seem very useful beyond user convenience, but considering the huge increase in mobile Web browsing, anything that helps save the device's battery power becomes very important. By using the PageVisibility API, your site can help the user's device consume less power and last longer.

The API specification, which as of this writing is in the Candidate Recommendation stage, provides both properties for detecting the document's visibility state as well as an event for responding to visibility changes. In this tutorial, I'll cover the basics of the API and show how to apply it to some practical examples (feel free to skip ahead to them if you're the impatient type).

Document Visibility Properties

The current version of the PageVisibilityAPI spec defines two document properties: the boolean hidden and the enumeration visibilityState. The visibilityState property currently has four possible values: "hidden", "visible", "prerender", and "unloaded".

Note: these properties are currently vendor-prefixed, so you’ll need to use prefixed versions such as "webkitHidden" and "webkitVisibilityState" until the spec has reached official recommendation status and the browser vendors implement the un-prefixed versions.

As you might expect, the hidden attribute returns true when the document is not visible at all. Typically, this means that the document is either minimized, on a background tab, the OS's lock screen is up, etc. The attribute is set to false if any part of the document is at least partially visible on at least one display. In addition, to accommodate accessibility tools, the hidden attribute can be set to false when a tool such as a screen magnifier completely obscures the document, but is showing a view of it.

Dealing with vendor prefixes

To keep the focus on the code instead of all the vendor-specific prefixing, I’m going to use some helper functions to isolate the browser-specifics.

function getHiddenProp(){
    var prefixes = ['webkit','moz','ms','o'];
    
    // if 'hidden' is natively supported just return it
    if ('hidden' in document) return 'hidden';
    
    // otherwise loop over all the known prefixes until we find one
    for (var i = 0; i < prefixes.length; i++){
        if ((prefixes[i] + 'Hidden') in document) 
            return prefixes[i] + 'Hidden';
    }

    // otherwise it's not supported
    return null;
}

Document Properties Example

Now we can write a cross-browser function, isHidden(), to see if the document is visible.

function isHidden() {
    var prop = getHiddenProp();
    if (!prop) return false;
    
    return document[prop];
}

For a more granular view of the document's visibility, you can use the visibilityState property. This property returns one of four values:

  • "hidden": the document is completely hidden from view
  • "visible": the document is at least partially visible on at least one display device
  • "prerender": the document is loaded off-screen and not visible (this value is optional; not all browsers will necessarily support it)
  • "unloaded": if the document is to be unloaded, then this value will be returned (this value is optional; not all browsers will necessarily support it)

The VisibilityChange Event

In addition to the visibility properties, there is a visibilitychange event that fires whenever the document's visibility state changes. You can register an event listener for this event directly on the document object:

Event Example

// use the property name to generate the prefixed event name
var visProp = getHiddenProp();
if (visProp) {
  var evtname = visProp.replace(/[H|h]idden/,'') + 'visibilitychange';
  document.addEventListener(evtname, visChange);
}

function visChange() {
   var txtFld = document.getElementById('visChangeText');

   if (txtFld) {
      if (isHidden())
         txtFld.value += "Tab Hidden!\n";
      else
         txtFld.value += "Tab Visible!\n";
   }
}

You can see the effects of the above code example here in the text edit field below. Try hiding and showing the tab containing this page and watch the edit field’s contents change:

(If you see the text "PageVisibilityAPI not supported!" then your browser doesn't support the API)

Practical Examples

Playing and pausing a video

This example shows how to use the PageVisibility API to pause and play a video. Start the video playing, then switch between tabs in your browser and note how (in supported browsers) the video pauses and plays as the tab's visibility changes.

This is accomplished by listening for the visibilityChange event and then toggling the state of the video:

window.addEventListener("load", function vidDemo() {
   sessionStorage.initialPlay = false;
   var vidElem = document.getElementById("video-demo");

   var visProp = getHiddenProp();
   if (visProp) {
      vidElem.addEventListener("play", function() {
         sessionStorage.initialPlay = true;
      });

      var evtName = visProp.replace(/[H|h]idden/,'') + 'visibilitychange';
      document.addEventListener(evtName, startStopVideo);
   }

   function startStopVideo() {
      if (document[visProp]) {
         vidElem.pause();
      }
      else if (vidElem.paused && sessionStorage.initialPlay == "true") {
         vidElem.play();
      }
   }
});

Only showing notifications when a tab is hidden

The W3C Notifications API allows you to display pop-up notification windows to the user to get their attention. When the user is already paying attention to the page, however, this can be pretty annoying. Using the PageVisibility API, you can elect to show these notifications only when the tab is hidden.

Enable notifications by clicking the "Enable Notifications" button. Then click the "Notify Me!" button, and switch away from the tab. After 5 seconds, the code will check to see if the tab is hidden and display a notification. Otherwise, a standard alert will be used.

(Click this first to make sure notifications are enabled)

And here's the code:

window.addEventListener("load", function notifyDemo() {
   var propName = "";
   var oNotify=null;
   var visProp = getHiddenProp();

   document.getElementById("notify-demo").addEventListener("click", function() {
      oNotify = null;
      if (window.webkitNotifications) {
         setTimeout(showNotification, 5000);

         if (window.webkitNotifications.checkPermission() == 0) { // 0 is PERMISSION_ALLOWED
            oNotify = window.webkitNotifications.createNotification('', 'Notification', 'You have been notified!');
         } 
      }
   });

   document.getElementById("notify-enable").addEventListener("click", function() {
      window.webkitNotifications.requestPermission();
   });

   function showNotification() {
      if (document[visProp] && window.webkitNotifications && oNotify) {
         oNotify.show();
      }
      else {
         alert("You have been notified!");
      }
   }
});

Deferring Google Analytics if the page is being pre-rendered

Some browsers, such as Google Chrome, have the ability to pre-render pages (you can read more about Chrome's pre-rendering here). This process involves downloading all the resources needed to render the page - including any scripts that the page contains. Many third-party sites use Google Analytics to detect when their page is being viewed, but this count can be skewed if a page is pre-rendered but never actually viewed by the user.

To prevent this problem, you can use the PageVisibility API to detect that your page is being pre-rendered and skip tracking the view if that is the case. Typically, your page will contain code that looks like this:

var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-XXXXX-X']);
_gaq.push(['_trackPageview']);

In this case, the _trackPageview event is always recorded, even if the page is not shown to the user. A script that detects pre-rendering would look like:

var bHavePV = getHiddenProp();
var bInitialShow = false;
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-XXXXX-X']);

if (bHavePV) {
   document.addEventListener("visibilityChange", handleVisEvt);
}
else {
   _gaq.push(['_trackPageview']); // track page view normally when PageVisibility is not present
}

function handleVisEvt() {
   if (document.visibilityState == "prerender") {
      _trackEvent("pagedata", "prerender"); // might be interesting to keep track of pre-rendering
   }
   if (document.visibilityState == "visible" && !bInitialShow) {
      bInitialShow = true; // don't trigger this code again
      _gaq.push(['_trackPageview']);
   }
}

Summary

Building a great Web application involves far more than just using the whiz-bang, eye catching features that users can see and interact with. A truly great application makes considerate use of the user's resources and attention, and the Page Visibility API is an important piece of that puzzle. To learn more about building resource-conscious Web applications, check out our other performance-related articles.

External References

Comments

0