Archive for the ‘Technical’ Category

jQuery: x800 faster .clean() function

Sunday, February 1st, 2009

As I mentioned in my previous post, we’ve noticed severe performance problems when loading large chunks (660KB) of HTML via an .ajax() call.

Short story short, .load calls .clean() which uses .trim() which sucks. In our case it was taking around 2.7s!

So how can we get rid of .trim()? Here’s the first attempt:

 	// Trim whitespace, otherwise indexOf won't work as expected
-	var tags = jQuery.trim( elem ).toLowerCase();
+	var tags = elem.replace(/^\s+/, '').toLowerCase();

This is great as it takes the code from 2700ms to about 440ms.

But we can do a lot better! Why are we doing a .toLowerCase() on a 660KB string just to test the first 4-5 chars?! Also, we use indexOf() that will have to scan the entire 660KB string just to see if the string begins with a given string — and we do that a 6-7 times!

Let’s get rid of all that unnecessary work:

                                // Trim whitespace, otherwise indexOf won't work as expected
-                               var tags = jQuery.trim( elem ).toLowerCase();
+                               var tags = elem.replace(/^\s+/, '').substring(0, 10).toLowerCase();

                                var wrap =
                                        // option or optgroup
@@ -906,11 +906,12 @@
                                if ( !jQuery.support.tbody ) {

                                        // String was a <table>, *may* have spurious <tbody>
-                                       var tbody = !tags.indexOf("<table") && tags.indexOf("<tbody") < 0 ?
+                                       var hasBody = /<tbody/i.test(elem);
+                                       var tbody = !tags.indexOf("<table") && !hasBody ?
                                                div.firstChild && div.firstChild.childNodes :

                                                // String was a bare <thead> or <tfoot>
-                                               wrap[1] == "<table>" && tags.indexOf("<tbody") < 0 ?
+                                               wrap[1] == "<table>" && !hasBody ?
                                                        div.childNodes :
                                                        [];

That brings it down to about 4ms! And I’ll bet you it makes the browser “feel” lighter too as it uses a lot less memory.

Size impact is tiny as well. The simpler first try adds just 6 bytes to the minimized version, or only 4 bytes to the gziped version. The second try adds 17 bytes on top of the first one (in the minimized version), or only 12 bytes to the gziped version.

You can track this patchset in ticket 4037.

jQuery: x1000 faster test for blank strings (on large strings)

Sunday, February 1st, 2009

We are making heavy use of jQuery, and we were glad to learn version 1.3 is so much faster than 1.2.

However, we discovered that loading a particularly large popup takes a long time, somewhere in the neighborhood of 6s! After profiling it a bit, we discovered that jQuery takes close to 3s just to process the returned HTML.

First thing that popped up on the radar was .trim(). There’s an independent effort to try to make it faster, but that can be improved only so much. It would be best if we could avoid it altogether, at least in core.

In fact, one large use case for .trim() is to test for blank strings, that is empty strings or ones made up only of whitespaces. Of course, that’s a much easier problem to solve, so it would seem best to expose a different function that does exactly that. I called that function .blank()

Here’s my first try at that function:

      blank: function( text ) {
              return !text ||  /^\s*$/.test(text);
      },

When used on a large string (660KB of HTML), this executes in about 10ms vs. .trim() at 2000ms!

At this point I sent in a patch, just to discover in the morning (it was 3:30AM when I did this) that we can do a lot better :)

Here is my second version of .blank():

      blank: function( text ) {
              return !text || !/\S/.test(text);
      },

This one executes in less than 0.5ms on the exact same monster string. Now that’s pretty sweet.

Given how much .trim() is used to test for a blank string, and how much lighter it is to implement, I think it should be exposed as part of the core.

As for the price we pay (in terms of size), it is absolutely tiny: 45 additional bytes for the minified version, or only 17 more bytes when it is gziped. Not bad of >1000x speed improvement.

This takes care of one use of .trim() in jQuery’s core. Stay tuned for the next one.

You can follow the faith of this patch in ticket 4036.

Here is the complete patch:

--- jquery-1.3.1-orig.js        2009-02-01 10:53:19.000000000 -0500
+++ jquery-1.3.1-d.js   2009-02-01 10:53:44.000000000 -0500
@@ -636,9 +636,7 @@

        // Evalulates a script in a global context
        globalEval: function( data ) {
-               data = jQuery.trim( data );
-
-               if ( data ) {
+               if ( !jQuery.blank( data ) ) {
                        // Inspired by code by Andrea Giammarchi
                        // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html
                        var head = document.getElementsByTagName("head")[0] || document.documentElement,
@@ -1050,6 +1048,10 @@
                return elem[ name ];
        },

+       blank: function( text ) {
+               return !text ||  !/\S/.test(text);
+       },
+
        trim: function( text ) {
                return (text || "").replace( /^\s+|\s+$/g, "" );
        },
Until next time,
Dimi