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 byanimation-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.