Wednesday, February 1, 2012

Delayed Image Loading on Long Pages

On EatDifferent, we let users track their meals with both text and pictures, and now that we have iPhone and Android apps, more and more users are using the mobile apps to quickly snap photos of their meals during the day. That means that the stream of updates on the home page includes a lot of images — a thumbnail for each user updating or commenting, and a thumbnail of each meal (and one user even takes photos of their blood glucose meter before each meal!). Here's a highly scientific diagram of that:

I realized that users were downloading all of these images for the stream, even though many of them were offscreen, and well, I felt bad for unnecessarily using their bandwidth and for making their experience slower. So, I decided to only load the images that were visible (or, nearly visible). Here's how I did it:

The HTML

Normally, when you write an IMG into your HTML, you assign the URL to the src attribute. I didn't want to do that anymore, though, since the browser would then load that URL, so I instead assign the URL to a data-src attribute — something the browser will ignore, but that my JavaScript can pick up. Here's what my template looks like:

<div class="stream-user-img">
 <a href="{{=log.user.profileUrl}}"><img data-src="{{=log.user.photoUrl}}"></a>
</div>

The JavaScript

Now that I have src-less IMG tags, I need a function to decide which IMGs to src-ify. Here's what the function needs to verify:

  • The IMG must have an empty src and a non-empty data-src attribute.
  • The IMG must be visible per CSS - not visibility:hidden or display:none.
  • The IMG must be user-visible, within the viewport. Or, as I realized, it should be nearly visible, so that users don't see the IMGs loading most of the time. I decided on 500 pixels as a reasonable near threshold, just by playing and seeing what felt right.

Here's what that looks like using jQuery:

function loadVisibleImages() {
  $('img').each(function() {
    if (this.src === '' && $(this).attr('data-src') && $(this).is(':visible'))) {
      if (inView($(this), 300)) {
        this.src = $(this).attr('data-src');
      }
    }
   });
}

function inView(elem, nearThreshold) {
  var viewportHeight = getViewportHeight();
  var scrollTop = (document.documentElement.scrollTop ?
          document.documentElement.scrollTop :
          document.body.scrollTop);
  var elemTop    = elem.offset().top;
  var elemHeight = elem.height();
  nearThreshold = nearThreshold || 0;
  if ((scrollTop + viewportHeight + nearThreshold) > (elemTop + elemHeight)) {
    return true;
  }
  return false;
}

Okay, now that we have that function, we need to call it at the appropriate times. It should be called when:

  • New IMG tags are written into the page (after data loading and template rendering, for example).
  • IMG tags go from hidden to visible.
  • IMG tags come into view after a user scroll.

I didn't find an easy way of programmatically detecting those first two cases, so I simply call loadVisibleImages() after my template rendering and visibility toggling (which only happens in two places), and then for the last case, I need to listen to the window scroll event. Unfortunately, I can't simply attach my callback to that event, as some browsers will fire it a lot and executing code after each event will slow down the page scrolling. Instead, I use Ben Alman's throttle library to only call my function every 500 milliseconds during the scroll event. Here's what that looks like:

$(window).on('scroll', $.throttle(500, loadVisibleImages));

The Result

After implementing this viewport-based image loading, my home stream went from loading 120 images to loading just 12 images — a much better experience for the browser and users. There are various improvements that could be made to this technique, like setting appropriate filler images for the different image types (a blank user pic for thumbnails, a meal icon for meals, etc) and maybe even using data URIs for those... but this works well enough for now. ☺

Tuesday, January 17, 2012

Website Monitoring Services

Update: Here's an article that reviews a bunch of services.

After posting the iPhone app for EatDifferent in the App store last week, I've seen a noticeable increase in new user signups — despite the fact that there's no signup through the app itself, only through the website. That means more traffic to the App Engine hosted webapp, and unfortunately last night, it meant more traffic than my webapp was configured to handle. My webapp went over quota at around 10:30pm and served 503s until midnight, at which point the App Engine quotas reset and it started fresh. I was sleeping at the time, blissfully unaware that it was over quota, and I didn't find out until the next morning, thanks to a personal email from a friend and early user.

Why didn't I find out? Well, I have a lot of error logging in my app, but 503s don't actually trigger the error logging, since that logging is in the app code itself, so I didn't see any errors come through my inbox. Most webapps are like that — when their host goes down, their logging goes down with it — so that's why many website monitoring services exist. These services live outside of your webapp (hopefully on a different hosting system altogether), ping it every so often to make sure its returning HTTP 200s as expected, and alert you when it's not. I knew about such services, but of course, I didn't think to set myself up with one until after I needed it. You live, you learn.

After beating myself up for a few hours in the morning, I started investigating website monitoring services — and by investigating, I mean that I asked my Twitter followers for their recommendations. Here's what they suggested:

All of those services offer standard downtime monitoring, but some of them also offer performance monitoring and offer their service as an API or SaaS. For my purposes, I wanted something easy and free, and since it doesn't hurt to have multiple monitoring services, I went with UptimeRobot and a Pingdom free plan. They both offer alerts over email, SMS, and Twitter, plus Pingdom offers an Android app with push notifications. It was easy to set them up, and (its funny saying this about a service) hopefully, I will never have to hear from them... But just in case, I'm glad they're around!

Monday, January 16, 2012

Testing Facebook Login with Selenium

On EatDifferent, I give users two options for signing up: Facebook, for people that like the convenience of using FB to sign in everywhere, and email+password, for people that hate Facebook. I see about 50/50 usage for these options, so they are both equally important — which means testing both of them are equally important.

Until today, however, I only had integration tests for email signup and login, because, well, I was afraid. All I have to do for email login testing is enter a few text fields, but for Facebook testing, I would need to navigate to the Facebook popup window and manipulate *their* DOM, and I would need a test user account that was really and a truly a test user (and not my cat, who actually is surprisingly active on Facebook). As I was making some changes that affected Facebook users today, I decided it was time to face my fears. Good thing I did, because testing Facebook login turned out to be pretty easy. Read on to see how I did it...

Create a Test User

In the land of old, creating a test Facebook user meant digging up an old email address and signing up with your cat's details. That kind of sucked, since you only have so many old email addresses, and it wasn't exactly Facebook TOU-friendly. Thankfully, Facebook introduced a way earlier this year to create multiple test users, both programmatically and from the developer dashboard. For the purpose of my tests, I only need one test user, so I created him semi-manually with these steps:

  • Go to http://developers.facebook.com/apps, and select your app. Click 'Edit app' and then 'Roles' on the left-hand side.
  • Under 'Test Users', click 'Add'. Select '1' and don't select 'Authorize this app' (if your purpose is to test authorization).
  • Once created, click 'Modify' in the 'Test Users' section. You'll see a table with information about the test user, like their name (which you can change to something snappier, like "Sharky Shark" :) and ID.
  • Now you need to find out the user's password, so you can enter that with Selenium. First, get an access token for your app by putting this URL in the browser and replacing the client_id and client_secret with your app key and app secret:
    https://graph.facebook.com/oauth/access_token?client_id=APP_KEY&client_secret=APP_SECRET&grant_type=client_credentials
  • Now paste this URL in the browser, replacing the ID with your test user ID, access_token with the string you just got, and password with your desired test password:
    https://graph.facebook.com/TEST_USER_ID?password=TEST_PASSWORD&method=post&access_token=ACCESS_TOKEN
  • Click 'Login' in the users table so that you're logged into Facebook as them. Visit their profile, take note of their email address, and change any desired fields (depending on what your app uses - I gave mine a profile pic and location).

Presto, now you have a test user.

Test the Facebook Popup

For my integration tests, I use Selenium with its Python API and Python's unittest module.

For my FacebookTests test case, I extend my BaseTests class and add two test methods, one for testing authorization and the other for testing login.


Those test methods use the FacebookDom helper class (an extension of my DomHelper class) for finding and manipulating the Facebook window and DOM. Note that after the user has granted the authorization, Facebook will no longer ask for authorization unless the user specifically revokes it - so my do_authorize_flow function checks to see if the auth button is actually there before trying to click it.


Ta-da, now you have a tested Facebook authorization and login flow! Some may argue that you shouldn't try to test 3rd party APIs because they may go down or be flaky or such and that you should mock them out instead, but I'm of the philosophy that integration tests should try to mimic the production environment as much as possible. And hey, I want to know if Facebook goes down, changes their response, or becomes flaky.

Friday, December 23, 2011

Reusing HTML/CSS/JS across Web & Mobile

At this point, there's only one of me working on EatDifferent. That's awesome because it means I get to learn everything about what it means to make a user-facing website, but it also means that I need to be careful about where I spend my time, since it is such a precious resource.

When I started it, EatDifferent was a web-only service, but I soon realized that when it comes to tracking your daily habits, users really want to be able to track on the go, from their mobile device. I realized I needed a real strategy for how I could offer it as a website, mobile-optimized website, and as multi-platform mobile app while keeping them all up-to-date and still having time to iterate on the core functionality. So I decided to find a way to reuse as much of my code across those offerings as possible. Here's how I do it:

Basic setup

Datastore

When a user logins in, I store their authentication in a session cookie. From then on, either my web app or mobile app can make XMLHttpRequests to fetch or save information for the user. In the mobile app, the calls are made over SSL (and in case you're wondering, the cross-domain restrictions aren't applied to files in Phonegap apps so that is not an issue).

HTML

The Flask microframework comes bundled with support for Jinja2 templates (like Django templates but better) so that's what I use for server-side templating in my web app. I mostly use the templates for includes/inheritance and not for variable rendering, as then I can keep my logic in JavaScript, making it easier to re-use that logic in the mobile app. For example, log.html extends from a base HTML and includes Jinja2 templates for the log sections.

I wanted to reuse much of the same HTML in my mobile app, so I use the Jinja2 template engine for it as well. My mobile app is actually one single HTML page, where each "page" is a DIV with a .mobile-page class, and many of the "pages" include Jinja2 templates (the same ones that are used by the web app). After I make changes to the base HTML or templates, I test them in a browser (using a Chrome extension to mimic a small screen), and then when I want to output them to a device, I render the templates using a script and copy them to the Android/iOS app folders.

CSS

I start off with Twitter Bootstrap for my CSS, because it makes for a slick but easy-to-customize foundation. I then use SASS for writing my own CSS rules, as I can write cleaner cross-browser CSS that way and can also do things like includes. I define my shared styles and variables in common.scss, and include that in web.scss (for the web app) and phonegap.scss (for the mobile app). I use CSS media queries in both common.scss and web.scss to define rules for smaller screens &emdash; some of those rules apply also to the mobile app, but some just to the web app.

JS

As I mentioned in an earlier post, I recently refactored my JavaScript to make it easier to share logic across the web and mobile app. I now have a shared.js for shared functionality, a web.js for web-specific functionality, and phonegap.js for mobile-specific functionality (makes sense, doesn't it? :). Since I also use different JS libraries across the web and mobile app versions, I use different rules in my Makefile to generate the final compressed JavaScript for the web and mobile apps.

Summary

Here's a Venn diagram that summarizes what's different and what's shared:

And here are screenshots comparing the log on the web versus in the mobile app:

I suspect that my HTML/CSS/JS for the mobile app version will diverge more as I try to make the app conform more to the expectations of mobile users (and iPhone users in particular), but I still like the idea of reusing as much of my code as I can. The less time I spend writing redundant code, the more time I can spend adding features and improving the EatDifferent service for all my users.

Friday, December 9, 2011

Using 3-Legged OAuth APIs with Flask

The Withings body scale is a nifty device — after you connect it to your wireless network and create an account, it wirelessly transmits your measurements to its site. It also tries to estimate your body fat ratio, so that even when your weight stays the same, you can see if your body fat ratio is changing. In the last week, I had several EatDifferent users start using a Withings scale to monitor their weight while they improve their eating habits, so I wanted to let them connect their accounts to their Withings accounts.

Fortunately, Withings offers an API for accessing user measurements. Their API uses OAuth for authentication and JSON for the responses. Since users could enter measurements at any time and they don't want developers polling their API constantly, they do the smart thing and let developers add subscriptions for each user, so that a URL on your server is pinged whenever there are updates for a user. The API is well-designed and standards-compliant, so I was looking forward to integrating with it.

The tricky part of using any OAuth API is setting up the flow — your site has to generate a request token from the OAuth provider, redirect the user to the OAuth provider to grant access, and then once the provider redirects back, your site has to exchange the request token for an access token and save the credentials. Finally, with those credentials, you can actually start using the API. Since the OAuth flow involves redirects and saving session information, the implementation varies depending on the server-side framework you're using.

For EatDifferent, I'm using the Flask Python microframework on Google App Engine, so I started my integration by looking for Flask OAuth examples. I started with the Flask OAuth extension, which wraps on top of the oauth2 library and provides various decorators for retrieving the session tokens and handling the redirect. The extension worked, but since I wanted to customize it more, I decided to go straight to the source and just write URL handlers and a Withings client library based on that oauth2 library. You can check out that gist to see the Withings client code, and read on for a description of my URL handlers.

To start off the authentication process, I have this authorize_withings() URL handler that creates a WithingsClient with my key and secret (provided by Withings on registration), gets a request token, saves them in the session as a tuple, and redirects to the authorization URL.

@app.route('/authorize-withings')
def authorize_withings():
    withings_client = withings.WithingsClient(WITHINGS_KEY, WITHINGS_SECRET)
    callback_url    = (util.get_host() + url_for('handle_withings_authorization') 
    request_token   = withings_client.get_request_token(callback=callback_url)
    session[SESSION_WITHINGS_TOKEN] = (
        request_token['oauth_token'],
        request_token['oauth_token_secret']
    )
    auth_url = withings_client.get_authorization_url(request_token) 
    return redirect(auth_url)

Withings should then redirect back to my handle_withings_authorization() URL handler that creates a WithingsClient (with the request token from the session), requests an access token, and saves the token as a property on the current User entity. By saving it in the datastore instead of session, I can access it at any time in the future too, not just for this session.

@app.route('/handle-withings-authorization')
def handle_withings_authorization():
    request_token   = session[SESSION_WITHINGS_TOKEN]
    withings_client = withings.WithingsClient(
        consumer_key       = WITHINGS_KEY,
        consumer_secret    = WITHINGS_SECRET,
        oauth_token        = request_token[0],
        oauth_token_secret = request_token[1])
    access_token       = withings_client.get_access_token(
        oauth_verifier = request.args['oauth_verifier'])
    withings_auth   = {
        'oauth_token':        access_token['oauth_token'],
        'oauth_token_secret': access_token['oauth_token_secret']
    }
    g.user.withings_auth   = util.serialize(withings_auth)
    g.user.withings_userid = access_token['userid']
    g.user.save()
    return redirect(url_for('device_settings'))

Whenever I want to use the Withings API for a user, I fetch their authentication information, create a WithingsClient, and fire off requests to the API. For example, I use this parse_withings() URL handler to respond to notifications from their API.

@app.route('/hook/parse-withings')
def parse_withings():
    user_id         = request.form.get('userid')
    user            = models.User.all().filter('withings_userid =', user_id).get()
    withings_auth   = util.deserialize(user.withings_auth)
    withings_client = withings.WithingsClient(
        consumer_key       = WITHINGS_KEY,
        consumer_secret    = WITHINGS_SECRET,
        oauth_token        = withings_auth['oauth_token'],
        oauth_token_secret = withings_auth['oauth_token_secret'],
        userid             = user.withings_userid)
    measurement_groups = withings_client.get_measurements(
        startdate=request.form.get('startdate'),
        enddate=request.form.get('enddate'))
    imports.import_withings(user, measurement_groups)
    return Response(status=200)

Now that I have the OAuth flow setup for Withings, I can easily support other APIs — whatever my users need most. ☺

Monday, December 5, 2011

Upgrading from jQuery Templates to jsRender

In making my recent port from jQuery to Zepto, I was the most worried about porting over from jQuery templates - I knew there were other templating engines out there (like mustache.js, which comes highly recommended) but I also know templating engines can be vastly different and I didn't want to spend a lot of time porting.

Fortunately, I found out that jQuery templates have actually been deprecated in favor of jsRender, a revision of the library that isn't dependent on jQuery. Perfect!

The templating language for jsRender is largely the same as jQuery templates, with a few purely aesthetic syntax changes and one logical difference — atleast from the perspective of the parts that I was using. The jsRender library is still in development, so the differences might change in the future, but in the meantime, I thought I'd write up a quick upgrade guide for anyone else porting over.

The operators are now prefixed with a "#". You can pretty much do a search and replace to port them over - {{if}} to {{#if}}, {{each}} to {{#each}}.

Previously, variables were outputted with ${var} - now, variables are outputted using the same double brackets as operators, like so: {{=var}} and {{=var.property}}. If you're using a server-side templating engine that also uses that bracket notation for templating, you will need to instruct the engine to ignore the jsRender templates. For Jinja2 templates, that means surrounding the script tags with {% raw %}{% endraw %}.

In addition, the syntax for specifying you don't want HTML escaping changed from {{html var}} to {{var!}} (notice the exclamation mark at the end). Finally, if you want to access the value of the currently referenced variable, the syntax changed from ${$value} to {{=$data}}.

To give you an idea of what an upgraded template would look like, here's some example data and templates:

var templateData = {
  label: 'Comments on post for Wednesday, Dec. 5th',
  comments: [
     type: 'freetext',
     creator: {fullName: 'Pamela Fox', profileUrl: 'http://everyday.io/user/1'},
     textHtml: 'Have you tried macademia nut oil instead? You can get it from <a href="http://www.amazon.com">.'
   ]
}

Using jQuery templates:

var dom = $('#stream-comment-tmpl').tmpl(templateData);
<script id="stream-comment-tmpl" type="text/x-jquery-tmpl">
  <span>${label}</span>
  {{each comments}}
  <div class="stream-comment">
    {{if type == 'highfive'}}
     <div>
      <span class="icon-highfive"></span>
      High five from <a href="${creator.profileUrl}">${creator.fullName}</a>!
     </div>
    {{else}}
     <a href="${creator.profileUrl}">${creator.fullName}</a>:
     {{html textHtml}}
    </div>
    {{/if}}
  </div>
  {{/each}}
</script>

Using jsRender templates:

var template = $.template('stream-comment-tmpl', document.getElementById('stream-comment-tmpl').innerHTML);
var html = $.render(templateData, template);
<script id="stream-comment-tmpl" type="text/jsrender-tmpl">
  <span>{{=label}}</span>
  {{#each comments}}
  <div class="stream-comment">
    {{#if type == 'highfive'}}
     <div>
      <span class="icon-highfive"></span>
      High five from <a href="{{=creator.profileUrl}}">{{=creator.fullName}}</a>!
     </div>
    {{else}}
     <a href="{{=creator.profileUrl}}">{{=creator.fullName}}"</a>:
     <div>{{=textHtml!}}</div>
    {{/if}}
  </div>
  {{/each}}
</script>

In jQuery templates, you could pass null as a value in an array and use the if operator to test whether it was defined. In jsRender, the template will not be called at all for a null value. Instead, you need to define objects in the array, set an object property to null, and check to see if that object property is defined. The reasoning behind the change is discussed more in this issue.

To give you an idea of the change, here's a before and after - notice I had to change the data format itself.

Using jQuery templates:

var templateData = ['Walked to work', null, null, 'Biked to work'];
<script id="notes-mini-tmpl" type="text/x-jquery-tmpl">
<div>
  {{each dates}}
    {{if $value}}
     ${value}
    {{else}}
     No notes for this date.
    {{/if}}
  {{/each}}
</div>
</script>

Using jsRender templates:

var templateData = [{notes: 'Walked to work.', {notes: null}, {notes: null}, {notes: 'Biked to work'}];
<script id="notes-mini-tmpl" type="text/jsrender-tmpl">
<div>
  {{#each dates}}
    {{#if notes}}
     {{=notes}}  
    {{else}}
     No notes for this date.
    {{/if}}
  {{/each}}
</div>
</script>

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. :)