Optimizations for the WEB: transitionEnd

The age of jQuery animations

 

Tired of using jQuery.animate?

Well, you should be. As a general rule of thumb, if what you are trying to accomplish is a visual effect that has no functional meaning, then you should implement it in pure CSS. If you need to involve JavaScript, then it’s most probably poorly designed in the first place. At least, that’s the rule I try to follow myself.

JavaScript is (in most cases) slower than CSS, it does use GPU acceleration whenever possible, but it is still a lot slower.

Take a look at the following example:

 

 

Any web developer noobie will tell you this is highly inefficient way of performing such a simple task, more complicated than it has to be and just plain stupid. CSS is actually more powerful than you might think, so in this particular case you’ll be far better off with just using the :hover selector.

Great. But :hover is no rocket science, it has been around for ages and even programs that should no be called browsers (yes Internet Explorer, I’m talking about you) support it, what about some more complex cases? Well, as the web grows, so does the demand for more features. Thus, the CSS3 specification was born. It has incredibly powerful features and selectors that have you covered for almost anything you might think of. And it also has a fairly decent browser support.

 

Can’t go with no JS at all!

 

Yes, sometimes you just need to throw in a little bit of JavaScript.

Say you want to open a new window or use AJAX to load some content on your page, but you have a nice CSS styled popup, which you need to hide before you do that. Of course, you want to use a nice fade out animation to hide the popup and not just mercifully throw it away.

Great, you can do all that using CSS, sure you’ll need a bit of JavaScript to actually apply the appropriate classes when the user clicks the close button, but apart from that all the eye-candy can be handled by pure CSS.

But you really want to load your AJAX after your CSS transition finishes! So, you have two options:

 

  • You could animate the popup using jQuery.animate and just use the callback setting to specify what needs to happen after the animation ends. But you already know how I feel about jQuery.animate :)
  • Your second option is to perform your CSS transition and use setTimeout to make the JavaScript sleep for the duration of the transition and then do your thing.

 

I’m not going to talk about the first case, but here’s why I think the second case is also not a very good idea.

You can never, I repeat, never trust durations. If you set your CSS transition duration to say 500ms and you call setTimeout with 500 as the second argument, chances are, the end of the transition and your callback function being run will most probably never happen simultaneously. That’s just because the browser has a lot of things to do and you didn’t really say you wanted it to synchronize these two events.

That is generally no big deal especially for more powerful computers, as the difference would be in the magnitude of milliseconds. However, if you have a chain of events, every one of which happens when the previous one finishes, then this “desynchronisation” between CSS and JavaScript will start showing pretty soon.

 

Ok, then what is a very good ( the best? ) strategy?

 

It depends on your specific situation, but here’s how I recently started doing these type of things, and so far it seems to be working really well.

 

Behold – the transitionend event!

 

This is basically an event that is fired every time a CSS transition ends for a particular element. It is also easily cancelable even by pure CSS, which is nearly impossible to implement for the setTimeout strategy:

 

In the case where a transition is removed before completion, such as if the transition-property is removed, then the event will not fire.

 

Browser compatibility is relatively good, especially in mainstream modern browsers, though a lot of them require prefixed event names. Of course support is not so good when it comes to Internet Explorer, you’ll need to have at least version 10, but you could always fallback to setTimeout.

 

Let’s see some code!

 

So, I’ve decided to make a simple jQuery plugin, that I could quickly use in my projects:

 

$.fn.transitionEnd      = function(callback) {
    var $this           = $(this);
    return $this.one('transitionend', callback);
};

 

And here’s the code in action:

 

 

Great, the event is attached when the button is clicked, fired when the transition ends and immediately detached. But as you might expect it is not that simple. There are several issues we’ll need to address here before calling it a day.

Multiple events

As the event is being attached when the button is clicked (you might also want to attach it just once, not on every click, but for the sake of the article let’s do it this way) it will be attached more than once if you click the button before the transition ends. Clicking the button a couple of times would eventually (when the transition finally ends) fire a lot of event handlers. To fix this, we could just detach any previous event handlers.

 

$.fn.transitionEnd      = function(callback) {
    var $this           = $(this);
    return $this.unbind('transitionend').one('transitionend', callback);
};

 

Prefixes?

Then, there is the issue that this code will not run in all browsers since some of them require prefixed event names, so we could just throw them all in there, so the event would be attached for any of the supported event names, which are: transitionend oTransitionEnd webkitTransitionEnd.

But, to spare myself from yet another JSFiddle fork I’ll stop myself right here and say, that this won’t really work, because new versions of Chrome support both transitionend and webkitTransitionEnd, so attaching a handler for both events would eventually result in the handler being fired twice each time.

A better approach would be to simply ask the browser which is the first supported event name and use that.

 

var detectTransitionEvent   = function() {
    var element             = document.createElement('dummy');
    var events              = {
        'transition':       'transitionend',
        'OTransition':      'oTransitionEnd',
        'MozTransition':    'transitionend',
        'WebkitTransition': 'webkitTransitionEnd'
    };
    for(var event in events) {
        if("undefined" != typeof element.style[event]) {
            return events[event];
        }
    }
    return null;
};
$.fn.transitionEnd      = function(callback) {
    var $this           = $(this);
    var event           = detectTransitionEvent();
    return $this.unbind(event).one(event, callback);
};

 

Properties

Hooray, it works!

So are we done?

Well.. no. Let’s make the button height animate as well, but a bit slower than the width, just a quick CSS fix :)

 

 

See how the callback is being fired when the width stops animating and not the height? Don’t panic, that’s completely normal. This happens, because we have attached the handler for the first occurrence of the event and since two properties are being animated, the event is fired twice, but the handler isn’t attached anymore when the second event fires.

So, we really just need to specify which property (or properties) we want to listen for. This way we can have multiple callbacks for muliple properties, or a single callback for a list of properties. We also have a special case (when leaving the property argument empty) which would fire the handler for the first event occurrence regardless of the property (for generally simple cases).

And one more thing, we should get rid of jQuery’s one method as it would introduce an issue. Since we are now deciding if the callback should be fired, we should also take care of when the handler should be detached. Or else, we might end up with a situation in which the event handler fires, but not the callback since it does not cover our requirements (wrong property), jQuery removes the event handler and when the moments comes there is actually no handler to be fired, which would call our precious callback.

 

var detectTransitionEvent   = function() {
    var element             = document.createElement('dummy');
    var events              = {
        'transition':       'transitionend',
        'OTransition':      'oTransitionEnd',
        'MozTransition':    'transitionend',
        'WebkitTransition': 'webkitTransitionEnd'
    };
    for(var event in events) {
        if("undefined" != typeof element.style[event]) {
            return events[event];
        }
    }
    return null;
};
$.fn.transitionEnd      = function(callback, propertyNames) {
    var $this           = $(this);
    var event           = detectTransitionEvent();
    var domElement      = $this.get(0);
    var wrapperCallback = function(e) {
        if("string" == typeof propertyNames) {
            if(-1 == propertyNames.split(' ').indexOf(e.originalEvent.propertyName)) {
                return;
            }
        }
        $(domElement).unbind(event);
        callback();
    };
    return $this.unbind(event).bind(event, wrapperCallback);
};

 

What about a fallback?

Hopefully you’ll never actually need to worry about a fallback situation, but let’s just throw in a setTimeout, you know, just for the fun of it. You are obviously going to have to specify an arbitrary timeout duration or pass it as an argument. Also, we need to make sure, that the event we are receiving is being sent by the current DOM element and not by any of it’s children.

Read about event bubbling.

 

var detectTransitionEvent   = function() {
    var element             = document.createElement('dummy');
    var events              = {
        'transition':       'transitionend',
        'OTransition':      'oTransitionEnd',
        'MozTransition':    'transitionend',
        'WebkitTransition': 'webkitTransitionEnd'
    };
    for(var event in events) {
        if("undefined" != typeof element.style[event]) {
            return events[event];
        }
    }
    return null;
};
$.fn.transitionEnd      = function(callback, propertyNames) {
    var $this           = $(this);
    var event           = detectTransitionEvent();
    
    if(null === event) {
        setTimeout(callback, 350);
        return $this;
    }
    
    var domElement      = $this.get(0);
    var wrapperCallback = function(e) {
        if(e.originalEvent.target !== domElement) {
            return;
        }
        if("string" == typeof propertyNames) {
            if(-1 == propertyNames.split(' ').indexOf(e.originalEvent.propertyName)) {
                return;
            }
        }
        $(domElement).unbind(event);
        callback();
    };
    
    return $this.unbind(event).bind(event, wrapperCallback);
};

 

Optimizations

Good, all we have to do now is some optimizations and beauty procedures and we have our final code. Enjoy!

 

https://gist.github.com/TonyBogdanov/eadd0fdfb238b0355486