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.Event
s:
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 thatjQuery.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.
Previous Post