Saturday, November 19, 2011

Porting from jQuery to Zepto

In my quest for better performance in my PhoneGap Android app, I finally decided to switch from using jQuery to Zepto, a framework that boasts a similar API with a much smaller footprint and additionally offers mobile specific touch events. Of course, since Zepto is designed to be lighter weight than jQuery, it does not offer all the same methods — and the methods that it does offer sometimes differ in their arguments. This makes sense but it means that porting from jQuery to Zepto requires learning how their differences affect your code, and how you can workaround them.

Since other developers might start going down the porting path too, I thought I'd write a post on the differences I encountered while porting. Because I reuse much of my application code between my Phonegap app and desktop web app and I'm not yet ready to eliminate jQuery in my desktop app, I wanted to port my code over in a way that would mean it would work with either jQuery or Zepto as a base framework. That effectively meant that I tried to keep my code the same, and simply add to the Zepto JS file where needed. I'll explain my ports below and then show my Zepto additions at the end.

This post got a bit long, so here are quick links to the sections:

I spent most of my time porting 3rd party jQuery plugin code, as they tend to use the more esoteric jQuery features, but I also found a few places in my own application code where I was using jQuery functionality that's missing from Zepto:

  • CORS:

    I use CORS (cross-domain XMLHttpRequests) to communicate from my mobile app to my server, and that means setting xhrFields in jQuery.ajax(), something that isn't yet supported in Zepto. Luckily, there's a patch for it, and I applied that. I also discovered a bug with sending empty strings in CORS requests, and I work around that by sending null instead.

  • scrollTop:

    I use jQuery.scrollTop() to reset the scroll in my mobile app on page transition, so I took and Zepto-ified the jQuery implementation.

  • position:

    I use jQuery.position() to calculate where to scroll to in some cases, and Zepto only supports offset(). I checked out the jQuery source code for calculating position and stuck that inside Zepto.

  • non-standard selectors:

    jQuery supports several convenience selectors that aren't defined in the w3 spec and simply special cases them in their code, and these won't work as selectors in Zepto. There are some that correspond to methods - ":first" is .first() in Zepto, and ":last" is last(). There are others with no equivalents, like ":hidden", and ":visible", and I just removed my usage of them or checked display values manually. (I didn't feel like checking the jQuery source for them.)

  • valueless attributes:

    There are times when I set and remove valueless attributes on form elements, like "checked", "readonly", and "disabled". In jQuery, I used .attr('disabled', false). That's apparently been replaced in jQuery 1.7 by .prop('disabled', false). Neither will work in Zepto, however, but removeAttr('disabled') will. Since I want my code working with both, I now have .attr('disabled', false).removeAttr('disabled'), just to be sure.

Porting plugins

I use five jQuery plugins in my mobile app, only one of which I wrote myself, and they took the longest to port. For all of these plugins, I started off by changing the final line in the file from '})(window.jQuery);' to '})(window.jQuery || window.Zepto);' and then hoped for the best.

  • ColorSlider:

    I wrote this plugin to give users a colorful slider (red to green), and the only jQuery function it was missing was outerWidth(), so I wrote one based on the jQuery implementation. In doing so, I discovered that the Zepto width() function includes the padding and border, which jQuery.width() doesn't, so I submitted a patch clarifying that in the Zepto docs.

  • jQuery Templates:

    I was pretty worried about this port, but then found out that the jQuery templates was being deprecated in favor of a non-jQuery dependent library, jsRender/jsViews. That library is in beta and the docs are minimal, but the templating syntax is largely the same, and the author responded quickly to my upgrade question. One thing to note, though: the jsViews library tries to take the global $ variable if it doesn't see jQuery defined, so you have to call $.noConflict() after loading it if you want to use $ with Zepto instead — but you don't want to call that if you are actually using jQuery. Here's what I have after I load my scripts: if (window.Zepto) $.noConflict();

  • timeago:

    This handy library turns timestamps into pretty times like "1 day ago" and also auto refreshes elements with timestamps on an interval. It relies on jQuery.trim, which I just copied from their codebase. Along with the DateInput plugin, it relies on the ability to store objects as data attributes (instead of just strings), and thankfully, there's a data.js in the Zepto codebase that I copied into my Zepto JS to handle that.

  • Twitter Bootstrap modal:

    This is a simple modal library that's designed to work with either jQuery or Ender(a lightweight package library), so it already relied on minimal jQuery features. It does attach functions to $.support, which Zepto doesn't define, so I added a one-liner to define it. It also uses $.proxy, which I copied from the jQuery codebase.

  • jQuery Tools DateInput:

    This was by far the hardest plugin to port, and I did look around briefly to see if I should just switch to a completely different pure JS datepicker library, but I quite like this one so I stuck with with it. First, it uses $.expr, a jQuery object that isn't actually documented and (from what rumors tell me) might actually go away soon. I wasn't actually using the results of that function, so I simply defined the object so that the code would not error out. It also uses jQuery.clone and Event.isDefaultPrevented, which were simple enough to write my own versions of.

    Now we get to the tricky parts - methods that exist in both frameworks but don't have the same interface or behavior. I filed bugs on most of these, but the Zepto team could decide to keep their interface the same for simplicity: Zepto doesn't handle multiple self-closing tags in a creation string the same way as jQuery (Issue 322), Zepto does not assign this to the current iterated object in each() (Issue 295), and Zepto does not include an option for deep copy in the extend() function, and Zepto doesn't handle non-DOM objects in expressions/events (Issue 321, That last one unfortunately required a bit of dirty finagling in the DateInput code itself, as its non obvious how to extend Zepto to support binding and firing events on non-DOM objects.

Summary: What's different

To summarize, the following jQuery functionality is missing from Zepto (and much of it is in the gist below):

  • scrollTop()
  • position()
  • ":first", ":last", ":hidden", ":visible"
  • prop()
  • outerWidth()
  • trim()
  • support()
  • proxy()
  • expr()
  • isDefaultPrevented()

And this functionality differs in behavior/arguments:

  • attr('disabled', false);
  • ajax() CORS
  • width()
  • data()
  • each()
  • extend()

My Zepto modifications

Most of the above issues were resolved by defining functions or objects in Zepto, and this gist shows all of those:

I also made a few tweaks to the core Zepto code, and you can see those in this diff (but not all of them are necessarily successful tweaks, nor are they tested.)

Performance comparison

Since my main point in porting over to Zepto was to improve my loading performance, I celebrated the successful porting by measuring the loading times for my page. I used this timing code and compared the differences between including a minified jQuery 1.7 script tag and a minified Zepto script tag, both for a fresh app install and an app re-launch. On average, switching to Zepto shaved 22% off my total loading time. For the detailed results, check out this spreadsheet.

Was it worth it?

I'm happy that I ported, but it's not a decision to be taken lightly. jQuery alternatives are still in early stages, so if you use them, you have to think of yourself as a beta user, be prepared for issues, be prepared for lack of documentation, and do the responsible thing — report whatever issues or workarounds you find. And on that note, thanks to the Zepto authors for bearing with my barrage of issues over the last few days. :)