<img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 "> Bitovi Blog - UX and UI design, JavaScript and Front-end development
Loading

Bitovi |

Faster jQuery.event.fix with ES5 Getters

Faster jQuery.event.fix with ES5 Getters

Justin Meyer

Justin Meyer

Twitter Reddit

If you turn on a profiler in most of the apps we've built and click around like a user, after a while you'll notice jQuery.event.fix is often taking the most time (in the video below, it takes 6.34% of the total time). Following the logic behind Amdahl's law, it makes sense that making jQuery.event.fix faster would have the greatest impact on our apps. This article walks through:

  • how jQuery normalizes events
  • why it has been slow
  • the ways it has been sped up, and
  • how using ES5 getters could speed it up even more.

How jQuery Normalizes Events

When an event is received by jQuery, it normalizes the event properties before it dispatches the event to registered event handlers. By normalizing, I mean it makes sure the event handler properties are the same across all browsers. For example, IE does not support event.relatedTarget, instead IE provides event.toElement and event.fromElement. jQuery uses those properties to set a relatedTarget property.

It might surprise you, but your event handlers aren't receiving a real event. Instead they are getting a new jQuery.Event with similar properties to a raw HTML event. jQuery does this because it can't set properties on a raw HTML event.

You can get the raw event with originalEvent like:

$("#clickme").bind("click", function( ev ) {
  ev.originalEvent
})

jQuery creates and normalizes the jQuery.Event from the raw event in jQuery.event.fix.

Why fix has been slow

Calling fix slow is inaccurate. In my basic test, fix can be called 50,000 times a second in Chrome - that's blazin. However, in most apps, events are involved in almost every execution path. This means jQuery.event.fix is called pretty much every time anything happens.

jQuery.event.fix works by copying each property of the raw HTML event to the newly minted jQuery.Event. This copying is where almost all of the expense comes from jQuery.event.fix.

I posted about this 2 years ago on jQuery's forums. Dave Methvin suggested using ES5 getters to avoid looking up the properties. Mike Helgeson made a run at it, but nothing came out of it.

How it has been sped up

For jQuery 1.7, Dave Methvin improved jQuery.event.fix considerably. It copies and normalizes only the event properties that are needed. It also uses a fast loop:

for ( i = copy.length; i; ) {
  prop = copy[ --i ];
  event[ prop ] = originalEvent[ prop ];
}

But it's still the slowest part of our apps. The following video shows Austin clicking around like a user in one of our apps with the profiler on. At the end of this speed up video, you'll see jQuery.event.fix is the slowest method of the app at 6.34%!

Speeding up jQuery.event.fix would have a big impact across the application. And, it can be done in one place.

Using ES5 getters

ES5 getters allow jQuery.event.fix to avoid copying every property and normalizing it for every event. Instead getters can do this on-demand. That is, they can lookup the originalEvent's value and normalize it if needed.

For example, the following defines a relatedTarget getter on jQuery.Events:

Object.defineProperty(jQuery.Event.prototype, "relatedTarget",{
  get : function(){
    var original = this.originalEvent;
    return original.relatedTarget ||
           original.fromElement === this.target ?
             original.toElement :
             original.fromElement;
  }
})

jQuery.event.fix could be changed to set up the jQuery.Event with the originalEvent, src, and target property like:

$.event.fix = function(event){
  // make sure the event has not already been fixed
  if ( event[ jQuery.expando ] ) {
    return event;
  }
  // Create a jQuery event with at minimum a target and type set
  var originalEvent = event,
      event = jQuery.Event( originalEvent );

  event.target = originalEvent.target;
  // Fix target property, if necessary (#1925, IE 6/7/8 & Safari2)
  if ( !event.target ) {
    event.target = originalEvent.srcElement || document;
  }

  // Target should not be a text node (#504, Safari)
  if ( event.target.nodeType === 3 ) {
    event.target = event.target.parentNode;
  }

  return event;
}

Note: jQuery.Event( originalEvent ) set the originalEvent and src properties. We set target because target is almost always going to be used.

When event.relatedTarget is called it calls the getter and returns the normalized value. We could add every property this way.

But there's a catch!

I brought this up to jQuery-maintainer and chief Rick Waldron and he shared this with me:

fun fact: getters are atrociously slow. http://jsperf.com/object-create-prop-attribs/2 This will likely never be in jQuery.

Buzz kill! Fortunately, we can be smart and cache the computed value for quick lookup the next time. My first naive try was like:

Object.defineProperty(jQuery.Event.prototype, "relatedTarget",{
  get : function(){
    var original = this.originalEvent;
    return this.relatedTarget = (original.relatedTarget ||
           original.fromElement === this.target ?
             original.toElement :
             original.fromElement);
  }
})

Notice the this.relatedTarget = .... I was hoping this would set a relatedTarget property on the jQuery.Event instance. This does not work because accessor descriptors are not writeable. But, we can use Object.defineProperty to set a data descriptor on the event instance like:

Object.defineProperty(jQuery.Event.prototype, "relatedTarget",{
  get : function(){
    var original = this.originalEvent,
    value =  (original.relatedTarget ||
              original.fromElement === this.target ?
                original.toElement :
                original.fromElement);
    Object.defineProperty(this, "relatedTarget",{
      value: value
    });
    return value;
  }
})

The final code goes through the list of properties that jQuery.event.fix copies:

  • $.event.keyHooks.props
  • $.event.mouseHooks.props
  • $.event.props

and creates getters for each one. In the getter, it checks if that prop is special (needs normalizing) and uses that prop's special function to normalize the value. It then uses the defineProperty-value trick to cache the result for fast lookup.

I created a basic JSPerf that shows a 3 to 4 times performance improvement. It compares my fix method vs jQuery's existing fix method and reads the event's pageX and pageY twice.

Conclusions

My measurement results are not perfect:

  • Although, the profiler indicates jQuery.event.fix is the slowest (speed x #-of-calls) part of our app, it does not count DOM interactions. It also betrays the fact that jQuery.event.fix is almost never the slowest part of any one user interaction.
  • The JSPerf only reads 2 properties. For a proper evaluation, a graph should be made of performance vs the number of properties read.

Despite this, from a library's perspective, improving jQuery.event.fix should be an easy and high-value target for jQuery. A simple change could improve our app's over-all performance by almost 3%. There are very few improvements in jQuery that could claim something similar.