One of the challenges of building for the web is the vast number of different devices and browsers that could access your site. But the challenge goes beyond just the sheer number of combinations. You also have no idea how many resources a user’s specific device/browser will have available, or what else it may be trying to do at the same time.

Because of this, sniffing for browsers, devices, or even browser features, only goes so far. Even higher-powered devices, with newer browsers, may still have issues at times—it all depends on what else they’re trying to do in any given moment.

So what do you do if you want to add some non-essential feature to a page (like an animation, for instance), that will work in most device/browser combinations, yet may be problematic on some—like lower-powered devices, or less performant browsers, or even higher-powered devices with limited resources available at that moment in time?

One route would be to add the feature for everyone—if it works for most users, those with less capable devices will just have to suffer. Another option would be to scrap it altogether—if it doesn’t work for everybody, don’t implement it at all.

Of course, you could limit what you try to do to only certain browsers by testing for specific functionality. And while this can be valuable, it also doesn’t guarantee what state the browser will actually be in when it hits the page, and whether it will be performant in a given moment.

An alternative approach would be to actually monitor the performance of the browser in real time, and then use those metrics to help determine whether you should continue to use, or to stop, the feature on that particular browser on that particular device at that particular moment in time.

Setting up a Jank Callback

Recently, I was working on a project which called for a canvas animation on one of the pages. Knowing that on some lower-powered devices and less performant browsers this animation could lead to jank, we wanted to integrate it in such a way so that 1) it would be used as intended on devices and browsers that could handle it, but 2) it would trigger a callback in cases where the browser couldn’t keep up. This would allow us to adjust the user experience on the fly if the situation merited it.

To do this, we used some JavaScript to track the real-time frames per second (FPS). Using requestAnimationFrame(), we kept a tally of the number of frames that were actually displayed every second, and then also tracked how many times this number dropped below a certain threshold. Once we saw performance had dipped below this threshold a certain number of times, the script would execute a callback. In this instance, the callback was used to stop the animation and give us the option to implement any necessary fallbacks.

    window.jankcb = function(callback) {
      console.log('Starting JankCB...');

      // Check for callback
      if (typeof callback !== 'function') {
        return;
      }

      // Check for rAF
      if (!window.requestAnimationFrame) {
        callback();
        return;
      }

      var delta = 0;
      var fps = 0;
      var lastTimestamp = 0;
      var ticks = 0;
      var misses = 0;
      var rAF = window.requestAnimationFrame;
      var minFps = 48;
      var maxMisses = 3;

      var updateFps = function(timestamp) {
        if (ticks === 0) {
          lastTimestamp = timestamp;
        }

        if (timestamp < lastTimestamp + 1000) {
          ticks += 1;
        } else {
          delta = timestamp - lastTimestamp;
          fps = ticks / (delta / 1000);
          console.log('FPS:', fps);

          if (fps < minFps) {
            misses += 1;
            console.log('Miss: ', misses);
            if (misses >= maxMisses) {
              console.log('Triggering Callback');
              callback();
              return;
            }
          }
          ticks = 0;
        }

        rAF(updateFps);
      };

      rAF(updateFps);
    };

With this function in place, we could then call jankcb() from elsewhere in the application, passing in any callback function we wanted to be executed if, or when, jank was happening.

    function customCallback() {
      // Actions to take if jank starts to occur...
    }

    window.jankcb(customCallback);

The jankcb() code above is logging the FPS and misses to the console, so we could see what was happening in real time. Here’s an example of what happened when we loaded the page in a browser with a throttled CPU (via DevTools).

Screenshot of console
Callback is executed after browser fails to maintain target FPS

The browser used for this test was a new machine with the latest version of Chrome. If we had just checked for what browser was being used, or what functionality it was capable of, we may have assumed that it would be able to run the animation with no problems. But since we were monitoring the actual FPS in real time, we knew within a few seconds that there would be ongoing problems if we continued running the animation.

Setting up a function like this allowed us to attempt to do some potentially processing-intensive animations on the page, knowing that if the device couldn’t keep up—for whatever reason—there would be a suitable fallback in place. Using real-time FPS metrics not only helped us identify when jank was occurring, but also gave us the ability to respond accordingly.


*Update 2018-07-11: JankCb.js, a JavaScript plugin based on this approach, is now available on GitHub.*