Cancel CSS3 and JS Animations to Prevent High Memory and CPU Consumption

So you got your creations gallery up on your site and shared the link on some social networks like twitter and facebook. Your loyal fans clicked your link and ended up crashing their browser. OMG! What happened ? Did you have any experiment with lots of amazing CSS3 and JS animations ? Most probably, that’s what happened then. Your experiments were awesome enough to eat up all the RAM and CPU and what not of your fan’s computer and crash it eventually.

Now What ?

Not to worry. There’s an easy but lengthy (in terms of code) solution to this problem. When the gallery web page loads and the iframes generate the small resolution previews of your creations, you can add some code in those previews to kill (cancel/pause) the CSS3 and Javascript animations after a small interval, like 3 seconds ?

What's the one thing every developer wants? More screens! Enhance your coding experience with an external monitor to increase screen real estate.

window.is_webkit = /(webkit)[ \/]([\w.]+)/.exec(window.navigator.userAgent.toLowerCase());
// Why the is_webkit check ? Because Firefox or IE might still cause
// performance issues if we wait for few seconds. But this is up to you really.
// Remove the check if you want to.
window.max_timer = window.is_webkit ? 3000 : 300;
// or just `window.max_timer = 3000;`

Killing Javascript Animations

Javascript animations are mostly achieved with setTimeout, setInterval and requestAnimationFrame. Let’s cancel them one by one. Wait, what ? Surely, Javascript provides us with clearTimeout, clearInterval and cancelAnimationFrame but how do we get the registered IDs from the experiment’s code that needs to be passed to these cancelling functions ?

Even I was quite unsure about how to do this, but Googling + StackOverflow helped me out figure out solutions for setTimeout and setInterval. Rest was easy to figure out.

Ok, Now pay attention here. We need to nail down our own implementation of setTimeout, setInterval and rAF that in turn will use the browser’s implementation. This way, we can store the IDs that these functions return into an array. After window.max_timer milliseconds, all we need to do is pass all the registered IDs to clearTimeout, clearInterval and cAF – Neat!

Relax and read some code now.

Re-defining setInterval

    window.setInterval = (function(oldSetInterval) {
      var registered = [];

      var f = function() {
        var id;
        // .. this!
        var $this = this;
        // setInterval accepts n no. of args
        var args = arguments;

        // If the first val is false, then don't register.
        // Just incase if we want to have our own setIntervals.
        if (typeof args[0] !== 'function' && args[0] === false) {
          args = Array.prototype.slice.call(arguments);
          args = args.slice(1);

          id = oldSetInterval.apply($this, args);
        }
        else {
          id = oldSetInterval.apply($this, args);
          registered.push(id);
        }

        // Need to return the Interval ID
        return id;
      };

      f.clearAll = function() {
        var r;
        while (r = registered.pop()) {
          clearInterval(r);
        }
      };

      return f;
    })(window.setInterval);

That was quite simple. We re-defined window.setInterval. Note how we passed the original window.setInterval to the function call whose returned value was assigned to our version. Now it points to a function that calls the original setInterval using apply(), i.e., our implementation basically passes all the arguments to the original version. Don’t know about apply and call ? Hmm…

The IDs returned gets pushed into the registered array. Now all we need to do is loop over the array and pass the IDs to clearInterval after the desired amount of milliseconds that we stored in window.max_timer. Note, we store the clearAll method on the function itself, i.e., our implementation of window.setInterval. So it has to be called like this – window.setInterval.clearAll(). Remember, functions (setInterval) are regular JS objects, so defining properties (clearAll) on them is possible.

As you might have already guessed, we’ll do the same with setTimeout.

Re-defining setTimeout

    window.setTimeout = (function(oldSetTimeout) {
      var registered = [];

      var f = function() {
        var id;
        // .. this!
        var $this = this;
        // setInterval accepts n no. of args
        var args = arguments;

        // If first val is false, then don't register.
        // Used for our own setTimeouts that we use to cancel
        // the JS and CSS Animations.
        if (typeof args[0] !== 'function' && args[0] === false) {
          args = Array.prototype.slice.call(arguments);
          args = args.slice(1);

          id = oldSetTimeout.apply($this, args);
        }
        else {
          id = oldSetTimeout.apply($this, args);
          registered.push(id);
        }

        // Need to return the Timeout ID
        return id;
      };

      f.clearAll = function() {
        var r;
        while (r = registered.pop()) {
          clearTimeout(r);
        }
      };

      return f;
    })(window.setTimeout);

Cancelling All setInterval and setTimeout

Clearing up is going to be easy now.

setTimeout(false, function() {
    setTimeout.clearAll();
    setInterval.clearAll();
}, window.max_timer);

Re-defining requestAnimationFrame

Similar approach as the previous one.

    // Time to Cancel rAF's Bro
    // Need to test for vendors!

    window.__requestAnimFrame = window.requestAnimationFrame || undefined;
    window.__cancelAnimFrame = window.cancelAnimationFrame || undefined;
    window.__vendors = ['webkit', 'moz', 'ms', 'o'];
    window.__registered_rafs = [];

    window.__requestFrame = function(cb) {
      // No support for rAF ?
      if (!window.__requestAnimFrame) return;

      var req_id = window.__requestAnimFrame(cb);
      __registered_rafs.push(req_id);

      return req_id;
    };

    // Determine the proper VendorPrefixedFunctionName
    if (!window.__requestAnimFrame) {
      for (var x = 0; x < window.__vendors.length; x++) {
          window.__requestAnimFrame = window[window.__vendors[x]+'RequestAnimationFrame'];
          window[window.__vendors[x]+'RequestAnimationFrame'] = __requestFrame;

          // Maybe we can remove the condition and just keep the code inside
          // because !window.__requestAnimFrame should be === !window.__cancelAnimFrame
          if(!window.__cancelAnimFrame) {
            // I came across webkitCancelAnimationFrame and webkitCancelRequestAnimationFrame
            // No idea about the difference, so maybe lets ||'fy it

            window.__cancelAnimFrame = window[window.__vendors[x]+'CancelAnimationFrame'] ||
                                        window[window.__vendors[x]+'CancelRequestAnimationFrame'];
          }
      }
    }

Cancelling All rAFs

    // Let's Cancel our rAFs
    setTimeout(false, function() {
      // No support ?
      if (!window.__requestAnimFrame) return;

      var r;
      while (r = window.__registered_rafs.pop()) {
        window.__cancelAnimFrame(r);
      }
    }, window.max_timer);

Notice: Did you note that we passed false as the first argument to all our cancellations ? That’s why we had that extra condition in our setTimeout and setInterval implementations to make sure our cancellation calls do not get registered and hence cleared out by any chance.

Killing CSS3 Animations

Thankfully, browsers give us access to animation events using which we can obtain useful information like name of the CSS animation and the time at which it occurred.

  • animationstart is fired when the css animation starts.
  • animationend is fired when the animation ends.
  • animationiteration is fired when the animation gets into a new iteration (specified by animation-iteration-count).

Too much talk, how is it useful to us anyway ? We can benefit from Event Bubbling. Naturally, these events are going to bubble, which means we can attach these events on the body and set the animation-play-state on the target element to paused. The full code:

    var stopCSSAnimations = function() {
      // Get the Body Element
      var body = document.getElementsByTagName('body')[0];

      // We'll setup animationstart and animationiteration
      // events only. No need for animationend, cuz the
      // animation might be 30minutes long. animationiteration
      // cuz the animation might be .000002ms long.

      // addEventListener is perfectly supported in IE9.

      // For Webkit (Chrome, Safari)
      body.addEventListener('webkitAnimationStart', stopAnimation, false);
      body.addEventListener('webkitAnimationIteration', stopAnimation, false);
      // For Mozilla and any other that supports the W3C version
      body.addEventListener('animationstart', stopAnimation, false);
      body.addEventListener('animationiteration', stopAnimation, false);
    };

    // e is the event object bro
    var stopAnimation = function(e) {
      var target_el = e.target;
      var e_type = e.type.toLowerCase();

      if (e_type.indexOf('animationstart') !== -1 || e_type.indexOf('animationiteration') !== -1) {
        // we need to stop the animation now!

        setTimeout(false, function() {

          if (target_el.style.webkitAnimationPlayState !== 'paused')
            target_el.style.webkitAnimationPlayState = 'paused';

          if (target_el.style.MozAnimationPlayState !== 'paused')
            target_el.style.MozAnimationPlayState = 'paused';

          if (target_el.style.animationPlayState !== 'paused')
            target_el.style.animationPlayState = 'paused';

        }, window.max_timer);
      }
    };

    stopCSSAnimations();

Again, we used our own implementation of setTimeout.

Celebrations!

That’s all guys! Accomplishments must be followed by celebrations. Go have cakes and chocolates.

Conclusion

  • We have learnt how to setup our sandbox to render html, css, js code.
  • We have learnt how to make it secured.
  • We have learnt how to display our creations in a gallery that other people can browse through.
  • And now, in this article, we have learnt how to make our gallery battery friendly by making it consume less memory and cpu leading to happy and healthy users.

The next article seems to be a good fit for discussing the server-side components for this app, as client-side is not enough. Ofcourse I won’t go into too much details, but definitely point out what’s required in the backend.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download

Author: Rishabh

Rishabh is a full stack web and mobile developer from India. Follow me on Twitter.

Leave a Reply

Your email address will not be published. Required fields are marked *