iPhone Web Optimization, Lesson 2: The Dreaded Request Count

In the previous post, we've had a closer look at the properties of the iPhone's GPRS, EDGE and 3G network link, coming to the conclusion that while bandwidth actually isn't so bad, propagation delay is. This means that while receiving a stream of bits is relatively quick, a transaction in the sense of "iPhone asks server for file, server sends file" is relatively expensive due to the somewhat large propagation delay. We closed with the tip to not stubbornly concentrate on getting the byte count down, but also (actually, more importantly) the count of web requests.

In this installment, I'll introduce some techniques of doing just that and to get the most out of every web request you'll have to make. Rather than providing a "copy and paste"-ready solution, we'll be focusing on concepts, not code.

In order to reduce the amount of web requests for your iPhone-optimized web content, it's important to understand its anatomy. From the perspective of individually loaded elements, we can divide a common web page into text content (made up of (X)HTML, CSS and possibly Javascript) and media content (images, sounds, etc.). Today, we'll look at how to reduce the amount of web requests related to serving text content.

Let's start with an imaginary sample web app that is supposed to be used on the iPhone. We'll call it "LibLook", a web app where you can enter the name of a book and see if it's available for renting at your local library.

LibLook consists of 3 individual pages, which are dynamically served from PHP scripts (Ruby, Perl, Python, whatever): A search form page, the search results page and an "About" page with a fancy photo of you with a party hat. Additionally, there are 2 stylesheets and one Javascript file that are imported into every page.

Now let's count the amount of web requests necessary to serve textual content for a typical usage scenario (we're intentionally omitting media data like images here, since they'll be handled in the next lession): Alice navigates to the LibLook search form (4 web requests: the form page, the 2 CSS files and the Javascript). Since she's visiting this web app for the first time, she wants to check out the "About" page now (1 web request - we assume that the CSS and Javascript files are cached). She snickers at your photo and returns to the search form (1 web request). Now she's entering the name of a book she desperately needs for a seminar and hits the "Search" button (1 web request). Sadly, the book's not available at the moment, so she locks her iPhone and lets her head hang.

In total, this amounts to 7 requests: 4 (X)HTML documents, 2 stylesheets and one Javascript (provided that caching worked). First, let's look at the (X)HTML documents.

Analyzing our documents, we see that we have 2 static pages (search form, about page) and one dynamically generated page (the search results) which are dependent on the user search terms (duh!).

Static pages are those that are "always the same", not dependent on any user input. So what we can do is serve them both at once! How do we do that? Well, with Javascript!

Rather than having two separate pages, create one new page with two top-level DIV elements. Into the first one, we'll put the search form, into the second one, we'll put the "About" page content. Rather than using hyperlinks to switch between the pages, we now use Javascript code: When the "About" link on the search form page is clicked, we simply set the search form DIV's "display" style attribute to "none" while setting the "About" DIV's one to "block". In the same way, we can switch back, too. It looks like clicking through from one page to other in a very fast way to the user, but internally, no additional data has to be transfered over the wireless link.

The popular iUI library implements this switching between DIVs together with a nice, iPhone-like "sliding to the left" type of animation, so you can just use that. Also, my VoodooPhone uses this technique to display "pages" within the bookmarklet, so you could also have a look at its source.

This way, we've already gotten rid of two web request, bringing the count down to 5 (since navigating from search form to "About" and back now doesn't need even a single additional web request).

Of course, this technique is best used if the individual pages are reasonably small, as to not load any additional large amounts of data that the user might not even navigate to (if your "About" page — that a user might only have a look at once in their whole usage history — weighs in at 1MB, you might not want to serve it together with the search form that is to be displayed every time a user visits the app). Still, however, note that a couple of kilobytes more or less won't do much of a difference, since, again, the bandwidth isn't so much adding to the "slowness" as the propagation delay is.

Before we look at the stylesheets and the Javascript file, let's talk about how the iPhone caches web content. Other than on your desktop browser, where the cache can be very large (several gigabytes), the iPhone only seems to cache content in volatile memory. This means that once MobileSafari runs out of RAM, it starts dropping older cache entries. Therefore, if the user navigates to a couple of other sites between visiting your app, it is highly likely that your stylesheets and scripts will be cache misses. Thusly, caching on the iPhone is mostly effective only during a single visit, which is a huge difference to your desktop browser, where appropriately marked files could be cached for months or even years!

Consequently, making your web app nicely cacheable isn't that important on the iPhone, so a good way to reduce the web request count dramatically is to serve both stylesheets and the Javascript inline. This is usually frowned upon for normal websites, but makes sense on the iPhone. In order to keep your app nicely organized, you can still use external stylesheets and scripts, but rather than linking to them from your (X)HTML documents, inline these files on-the-fly (server-side) by using PHP's readfile method (or your favorite language's equivalent). Such a trivial optimization, but now our request count is already down to 2, shaving another 3 requests off due to the inlining. Awesome!

We've introduced one inefficiency now, though: Since the scripts and stylesheets are no longer externally linked (and thusly cached within the same session), we have to include them inline on both the search form/about page and the dynamically generated search results page. That's a waste of bandwidth!

AJAX to the rescue! As the final optimization, we change the "search results" script from creating its own independent page to creating just a new DIV (or whatever other block element is appropriate) and adding it to the search form page's DOM dynamically via Javascript (for example, using the innerHTML JS property). With this technique, we don't need to serve the inlined scripts and stylesheets again since the user basically stays on the same page now during their whole visit, even if they conduct multiple searches: Not only do we load the scripts and stylesheets only once, but the user can now go back to the search form to do another search without another page load, since we can just re-display the form using, again, Javascript, the same way we switch between the "About" and "search form" page. Pretty neat, ain't it?

iUI already comes with Javascript methods to load subpages AJAX-style, so you can use that. Additionally, it's very easy to roll your own, light-weight AJAX page loader, so this shouldn't be holding you back either.

To sum it up, we've now reduced our web request count for the sample scenario from a hefty 7 to just 2, all without adding a single additional byte in total! If you'd compare the perceived "speediness" of the old and new versions over 3G, EDGE or even GPRS, you'll notice that the optimized version will feel dramatically faster.

Looking back, this is what we did in a slimmed-down list:

  • Serving static pages together, switching with Javascript.
  • Inlining scripts and stylesheets
  • Using AJAX, so that the user stays on the same page.

A couple of thoughts before finishing this post: Not all of these techniques might be applicable to your app, depending on its usage structure and complexity. Still, the overall ides should be helpful.

Also, what about the 5 iPhone users that have switched off Javascript in MobileSafari? Since all 3 techniques rely on Javascript (apart from the inlining), you'd have to either ignore these users or check for Javascript and fall back to an unoptimized version if it's off. Note, though, that inlining stylesheets might still be applicable, if the sheets are sufficiently small (only a couple of kilobytes). This is because bandwidth isn't so much of an issue as propagation delay, so the bandwidth-penalty of inline-loading the sheets on multiple pages might be worth it.

You can see all of the 3 techniques in action in my Quick ORF and VienNav apps, both of which are built on top of iUI: The "About" page is already included in the index page, the scripts and stylesheets are inlined and the requests are loaded via AJAX. Both apps use the absolute minimum amount of web requests, making them feel pretty fast even when loading them over GPRS.

Of course, there are many more techniques to provide for more perceived page loading speed, some of them not limited to iPhone apps. Yahoo's performance tips](http://developer.yahoo.com/performance/) are an awesome resource on this topic. Additionally, here are some ideas that might be worth thinking about, too:

  • Predictively prefetching data via AJAX
  • Serving zlib-compressed pages
  • Packing/minifying your Javascripts and stylesheets
  • Using higher JPG compression
  • Using JPG for photos, but PNG for icons
  • Getting rid of unnecessary (X)HTML markup (by using CSS better)
  • Putting Javascripts at the bottom of the page
  • Doing more work client-side with Javascript, such as form validation.
  • Using shorter CSS class names and DOM element IDs (they can sum up!)

In the next installment, we'll talk about how to reduce the web request count for your media files (mostly image files in case of the iPhone).

UPDATE: Next lesson now online

Previous iPhone Web Content Optimization Lessons
Lesson 1: The Thin Pipe

PHP 'readfile' function
Quick ORF