Optimizing AJAX for iPhone

With the mobile web, you want your users to have an efficient web experience. This means getting your pages updated quickly and a good way to do that is by using AJAX, since it allows for dynamic content without page reloads. The Safari browser has great support for AJAX, so iPhone—which includes the intelligent Safari browser—can use AJAX to update pages without requiring the download of all of the HTML for the page.

The common AJAX patterns that work on the desktop can require some bulky downloads. This article shows you how to use optimization techniques to reduce the size of AJAX downloads so that you can speed up your pages and give your customers the responsive mobile experience they are looking for.

To demonstrate various AJAX techniques we are going to use a simple contact management database application. It will show not only how to transfer the data efficiently, but how to use some of the cool features of the phone as well.

This is a PHP-based set of dynamic pages that send back a list of contacts that includes names, emails, phone numbers and addresses. The MySQL for the database is shown in Listing 1.

Listing 1: addresses.sql

DROP TABLE IF EXISTS addresses;

CREATE TABLE addresses (
	id INTEGER NOT NULL AUTO_INCREMENT,
	name VARCHAR(255) NOT NULL,
	email VARCHAR(255) NOT NULL,
	phone VARCHAR(255) NOT NULL,
	address VARCHAR(255) NOT NULL,
	PRIMARY KEY ( id )
);

INSERT INTO addresses VALUES ( null, 'Jack Herrington', 'jack@example.com', '800-555-1212', '1 Infinite Loop, Cupertino CA' );
INSERT INTO addresses VALUES ( null, 'Lori Herrington', 'lori@example.com', '800-555-1212', '1 Infinite Loop, Cupertino CA' );
...

This isn't a particularly complex database. It's just a single table that has a list of contacts where each contains a name, email, phone number and address. A unique ID allows each record to be uniquely identified and we'll use that ID later in the article to demonstrate how to use caching to reduce the amount of data required to update the page.

There are two traditional AJAX methods for updating a page. The first is to return new HTML for a section of the page that needs to be updated. The second is to return data encoded as XML then to use Javascript on the page to interpret the XML and update the page. The XML system has the advantage that clients other than HTML pages can use the XML service to retrieve data, so it's generally preferred.

Listing 2 shows the PHP code required to export the database as XML. we'll use this code and the corresponding HTML page that reads the XML as a baseline so that we can compare the efficiency of XML versus the optimized data transfer techniques that follow.

Listing 2: xmllist.php

<?php
require_once("MDB2.php");
$dsn = 'mysql://root@localhost/addresses';
$mdb2 =& MDB2::factory($dsn);
if (PEAR::isError($mdb2)) { die($mdb2->getMessage()); }

$dom = new DomDocument();
$dom->formatOutput = true;

$root = $dom->createElement( "contacts" );
$dom->appendChild( $root );

$res =& $mdb2->query( "SELECT * FROM addresses" );
if (PEAR::isError($mdb2)) { die($mdb2->getMessage()); }
while ($row = $res->fetchRow(MDB2_FETCHMODE_ASSOC))
{
  $rec = $dom->createElement( "contact" );
  $elID = $dom->createElement( "id" );
  $elID->appendChild( $dom->createTextNode( $row['id'] ) );
  $rec->appendChild( $elID );
  $elName = $dom->createElement( "name" );
  $elName->appendChild( $dom->createTextNode( $row['name'] ) );
  $rec->appendChild( $elName );
  $elPhone = $dom->createElement( "phone" );
  $elPhone->appendChild( $dom->createTextNode( $row['phone'] ) );
  $rec->appendChild( $elPhone );
  $elAddress = $dom->createElement( "address" );
  $elAddress->appendChild( $dom->createTextNode( $row['address'] ) );
  $rec->appendChild( $elAddress );
  $elEmail = $dom->createElement( "email" );
  $elEmail->appendChild( $dom->createTextNode( $row['email'] ) );
  $rec->appendChild( $elEmail );
  $root->appendChild( $rec );
}
$res->free();

$mdb2->disconnect();

header( "Content-type: text/xml" );
echo $dom->saveXML();
?>

This fairly straightforward PHP, it opens a connection the MySQL database and runs a query to get all of the rows from the table. The code then creates and XML document object and adds tags to it to hold the data. At the end of the code it outputs the completed XML document tree.

When we run this on the command line we get the output shown below:

<?xml version="1.0"?>
<contacts>
  <contact>
    <id>1</id>
    <name>Jack Herrington</name>
    <phone>800-555-1212</phone>
    <address>1 Infinite Loop, Cupertino CA</address>
    <email>jack@example.com</email>
  </contact>
  ...
</contacts>
%

As you can see, it's even formatted nicely. But is it efficient? Let's find out by creating a page that retrieves the XML and updates the page to match. That code is shown in Listing 3.

Listing 3: http_xml.php

<html>
<title>XML test</title>
<head>
<meta name="viewport" content="width=device-width;user-scalable=no;">
<style>
body { margin: 0px; padding:0px; font-family: arial,verdana; text-decoration: none; color: black; }
.address { padding-left: 20px; font-size: large; font-style:italic; text-decoration: none; color: black; }
.phone { padding-left: 20px; font-size: large; text-decoration: none; color: black; }
.name { font-size: x-large; font-weight: bold; text-decoration: none; color: black; }
#dataBody td { border-bottom: 1px solid #ccc; }
</style>
<script>
var req = null;
var g_startTime = 0;

function addItem( name, phone, email, address ) {
  var dobj = document.getElementById( 'dataBody' );
  var elTr = dobj.insertRow( -1 );
  var elTd = elTr.insertCell( -1 );

  var elNameAnchor = document.createElement( 'a' );
  elNameAnchor.href = '#';
  elNameAnchor.className = 'name';
  elNameAnchor.onclick = function() { window.location = 'mailto:'+escape(email) };
  elNameAnchor.appendChild( document.createTextNode( name ) );
  elTd.appendChild( elNameAnchor );
  elTd.appendChild( document.createElement( 'br' ) );

  var elAddressAnchor = document.createElement( 'a' );
  elAddressAnchor.href = '#';
  elAddressAnchor.className = 'address';
  elAddressAnchor.onclick = function() { window.location = 'http://maps.google.com/maps?q='+escape(address) };
  elAddressAnchor.appendChild( document.createTextNode( address ) );
  elTd.appendChild( elAddressAnchor );
  elTd.appendChild( document.createElement( 'br' ) );

  var elPhoneAnchor = document.createElement( 'a' );
  elPhoneAnchor.href = '#';
  elPhoneAnchor.className = 'phone';
  elPhoneAnchor.onclick = function() { window.location = 'tel:'+escape(phone) };
  elPhoneAnchor.appendChild( document.createTextNode( phone ) );
  elTd.appendChild( elPhoneAnchor );
  elTd.appendChild( document.createElement( 'br' ) );
}

function processReqChange() {
  if (req.readyState == 4 && req.status == 200 && req.responseXML ) {

    document.getElementById('downloadTime').innerText = ( (new Date()).valueOf() - g_startTime ) + ' ms';
    document.getElementById('downloadSize').innerText = req.responseText.length + ' bytes';

    var buildStart = (new Date()).valueOf();

    var nl = req.responseXML.getElementsByTagName( 'contact' );
    for( var i = 0; i < nl.length; i++ ) {
      var nli = nl.item( i );
      addItem( nli.getElementsByTagName('name').item(0).firstChild.nodeValue,
        nli.getElementsByTagName('phone').item(0).firstChild.nodeValue,
        nli.getElementsByTagName('email').item(0).firstChild.nodeValue,
        nli.getElementsByTagName('address').item(0).firstChild.nodeValue );
    }

    document.getElementById('buildTime').innerText = ( (new Date()).valueOf() - buildStart ) + ' ms';
  }
}

function loadXMLDoc( url ) {
  req = new XMLHttpRequest();
  req.onreadystatechange = processReqChange;
  req.open('GET', url, true);
  req.send();
  g_startTime = (new Date()).valueOf();
}

var url = window.location.toString();
url = url.replace( /http_xml.html/, 'xmllist.php' );
loadXMLDoc( url );
</script>
<body>
<table cellspacing="0" cellpadding="3" width="320">
<tbody id="dataBody">
</tbody>
</table>
<br/>
<table>
<tr><td>Download time</td><td id="downloadTime"></td></tr>
<tr><td>Build time</td><td id="buildTime"></td></tr>
<tr><td>Download size</td><td id="downloadSize"></td></tr>
</table>
</body>
</html>

As convoluted as it looks this code is pretty standard AJAX. The loadXMLDoc function creates the AJAX request and the processReqChange monitors the request to check for completion. Once the XML has been downloaded completely the processReqChange function uses the XML DOM function to read all of the<contact> tags and add rows into the data table to display the contacts.

The code also monitors how long all of this took and how much data was downloaded. These metrics are shown in the table at the bottom of the page. When we bring this up in Safari on our iPhone we see something like Figure 1.


The XML testing HTML page

Figure 1: The XML testing HTML page

We've done some work to make this a more iPhone-friendly page. The <meta> tag at the top of the page defines the width of the page. The addItem() function adds the contact items to the table. The big name link starts an email by using the 'mailto:' link type. The address link right below that brings up a Google Map on that location. And the phone number starts up a phone call to that number by using a 'tel:' link. These CSS at the top of the page defines these text blocks in a fairly large font that makes for finger friendly targets.

From the efficiency standpoint the most important metric here is the download size, which is 1017 bytes. If we can trim that number down then we can get more data to the browser more quickly and provide a much more responsive service.

A very popular alternative to XML is the Javascript Object Notation (JSON) syntax. There are two big advantages to JSON—it's smaller and it's easier to read. The only downside is that JSON isn't a particular good format for non-Javascript clients. That's a trade-off that you will have to decide for yourself.

Let's get some metrics to decide just how much more efficient JSON is. The first JSON code is shown in Listing 4.

Listing 4: jslist1.php

<?php
require_once("MDB2.php");

header( "Content-type: text/javascript" );

$dsn = 'mysql://root@localhost/addresses';

$mdb2 =& MDB2::factory($dsn);
if (PEAR::isError($mdb2)) { die($mdb2->getMessage()); }

$res =& $mdb2->query( "SELECT * FROM addresses" );
if (PEAR::isError($mdb2)) { die($mdb2->getMessage()); }

echo( "[" );

$first = true;
while ($row = $res->fetchRow(MDB2_FETCHMODE_ASSOC)) {
  if ( !$first ) echo( ', ' );
  echo( "{id:".$row["id"].", email:'".$row["email"]."', address:'".$row["address"]."', name:'".$row["name"]."', phone:'".$row["phone"]."'}");
  $first = false;
}

echo( "]\n" );

$res->free();

$mdb2->disconnect();
?>

All of the SQL code is the same. The change comes when we output the data. Instead of creating an XML document we manually 'echo' the data in JSON format. When we run this on the command line we get the output shown below:

% php jslist1.php
[{id:1, email:'jack@example.com', address:'1 Infinite Loop, Cupertino CA', name:'Jack Herrington', phone:'800-555-1212'}, {id:2, email:'lori@example.com', address:'1 Infinite Loop, Cupertino CA', name:'Lori Herrington', phone:'800-555-1212'}, ...]
%

As you can see right away this is a tighter format than XML. The start and end tags are gone, and most of the whitespace has been removed. Where the XML data size was 1017 bytes, this JSON data is only 608 bytes for the same data, a 41% savings.

One problem with this PHP code is that it could return invalid JSON code if either the name, address, email or address contain quotes or other special characters. To solve this we use the JSON encoding function built into PHP. This is shown in Listing 5.

Listing 5: jslist2.php

<?php
require_once("MDB2.php");
...

$data = array();
while ($row = $res->fetchRow(MDB2_FETCHMODE_ASSOC)) {
  $data []= array( 'id' => $row["id"], 'phone' => $row["phone"],
    'email' => $row["email"], 'name' => $row["name"], 'address' => $row["address"] );
}

echo( json_encode( $data )."\n" );

$res->free();
$mdb2->disconnect();
?>

You know JSON is well supported when it's built into the language!

The HTML page that reads from the JSON data source is shown in Listing 6.

Listing 6: http_js.html

<html>
<title>Javascript test</title>
<head>
...
<script>
var req = null;
var g_startTime = 0;

function addItem( name, phone, email, address ) { ... }

function processReqChange() {
  if (req.readyState == 4 && req.status == 200 ) {

    document.getElementById('downloadTime').innerText = ( (new Date()).valueOf() - g_startTime ) + ' ms';
    document.getElementById('downloadSize').innerText = req.responseText.length + ' bytes';

    var buildStart = (new Date()).valueOf();

    var nl = eval( req.responseText );
    for( var i = 0; i < nl.length; i++ ) {
	  addItem( nl[i].name, nl[i].phone, nl[i].email, nl[i].address );
    }

    document.getElementById('buildTime').innerText = ( (new Date()).valueOf() - buildStart ) + ' ms';
  }
}

function loadJavascript( url ) {
  req = new XMLHttpRequest();
  req.onreadystatechange = processReqChange;
  req.open('GET', url, true);
  req.send();
  g_startTime = (new Date()).valueOf();
}

var url = window.location.toString();
url = url.replace( /http_js.html/, 'jslist2.php' );
loadJavascript( url );
</script>
...

Most of the code remains the same. The browser retrieves the JSON just the same way it retrieves the XML in the previous example. The difference is that this HTML code uses the 'eval' function in Javascript to turn the JSON encoded text directly into Javascript arrays and objects.

Not only is JSON 41% more efficient in this example, but it's also more efficient for the browser to parse. XML searching in Javascript is slow. But evaluating the JSON data is very quick which makes for an even more responsive page.

Caching

We could, and will, optimize the data transfer some more, but are there optimizations we can do on the HTML side? Certainly. One is to let the server know that data we already have displayed so that it only needs to send us any new contact data.

The code in Listing 7 shows a new version of the server script which takes an ID as an argument.

Listing 7: jslist3.php

<?php
require_once("MDB2.php");
header( "Content-type: text/javascript" );
$dsn = 'mysql://root@localhost/address';
$mdb2 =& MDB2::factory($dsn);
if (PEAR::isError($mdb2)) { die($mdb2->getMessage()); }

$since = 0;
if ( array_key_exists( 'since', $_REQUEST ) )
   $since = $_REQUEST['since'];

$sth =& $mdb2->prepare( "SELECT * FROM address WHERE id > ?" );
if (PEAR::isError($mdb2)) { die($mdb2->getMessage()); }

$res = $sth->execute( array( $since ) );

$data = array();
while ($row = $res->fetchRow(MDB2_FETCHMODE_ASSOC)) {
  $data []= array( 'id' => $row["id"], 'phone' => $row["phone"],
    'email' => $row["email"], 'name' => $row["name"], 'address' => $row["address"] );
}

echo( json_encode( $data )."\n" );

$res->free();
$mdb2->disconnect();
?>

The code then only returns records that have an ID greater than that of the one specified. Since IDs are always ascending we know that new contact records will always have an ID greater than older contact records. This means that the Javascript code on the page can keep going back to the server over and over again and only get new data when it becomes available. If there is no new data then the server will reply with only an empty array ( i.e., "[]"). That's two bytes to tell the client that there is no new data available.

Listing 8: http_caching.html

<html>
<title>Caching example</title>
...
<script>
var req = null;
var g_startTime = 0;
var g_lastid = 0;

function addItem( name, phone, email, address ) { ... }

function processReqChange() {
  if (req.readyState == 4 && req.status == 200 ) {

    document.getElementById('downloadTime').innerText = ( (new Date()).valueOf() - g_startTime ) + ' ms';
    document.getElementById('downloadSize').innerText = req.responseText.length + ' bytes';

    var buildStart = (new Date()).valueOf();

    var nl = eval( req.responseText );
    for( var i = 0; i < nl.length; i++ ) {
      g_lastid = parseInt( nl[i].id );
	  addItem( nl[i].name, nl[i].phone, nl[i].email, nl[i].address );
    }

    document.getElementById('buildTime').innerText = ( (new Date()).valueOf() - buildStart ) + ' ms';
  }
}

function loadJavascript( url ) {
  req = new XMLHttpRequest();
  req.onreadystatechange = processReqChange;
  req.open('GET', url, true);
  req.send();
  g_startTime = (new Date()).valueOf();
}

window.setInterval( function() {
  var t = (new Date()).valueOf();
  var url = window.location.toString();
  url = url.replace( /http_caching.html/, 'jslist3.php?since='+g_lastid+'&t='+t );
  loadJavascript( url );
}, 1000);
</script>
...

Most of the code remains the same but there is a modification when the code reads the JSON data to track the last ID seen. The big change is near the end of the script where we set a timer to call back to the server every second. That code then builds a URL that has specifies the 'since' URL in the argument with the last ID the page has displayed. It also adds a fake 't' variable with the value of the current time. This ensures that the Safari browser won't do any caching of the response from the server.

Even though we are setting an interval here it's important to realizethat Safari doesn't service timers unless it's the active application on the phone.

Transport Using Script Tags

As if both space and speed bonuses of using JSON aren't enough, there is yet another advantage; using the script tag transport mechanism. This advantage is all about security. Most browsers, including Safari, restrict the use of AJAX to accessing data from the domain of the page that is using the AJAX. For example, a page from http://www.mydomain.com cannot access data from http://www.apple.com. One way to get around this is to dynamically generate script tags in the document to load the data. Script tags can access code from any domain. This is how you can create widgets that can display data from your service on any page.

It starts with a unique form of page that returns JSON encoded within a Javascript callback. The updated code is shown in Listing 9.

Listing 9: jscallback.php

<?php
...

echo( 'addresses_callback('.json_encode( $data ).");\n" );

...
?>

There isn't much difference. We query the data from the database the same way as before. But instead of just returning the result of json_encode we wrap it in an invocation of the 'addresses_callback' function that needs to be defined by the Javascript code on the page.

When we run this on the command line it looks like this:

% php jscallback.php 
addresses_callback([{"id":"1","phone":"800-555-1212","email":"jack@example.com","name":"Jack Herrington","address":"1 Infinite Loop, Cupertino CA"}, ...]);

Show in Listing 10 is the updated code that reads the data using a dynamically generated script tag.

Listing 10: http_caching.html

<html>
<title>Javascript callback test</title>
...
<script>
var g_startTime = 0;

function addItem( name, phone, email, address ) { ... }

function addresses_callback( nl ) {
  document.getElementById('downloadTime').innerText = ( (new Date()).valueOf() - g_startTime ) + ' ms';

  var buildStart = (new Date()).valueOf();

  for( var i = 0; i < nl.length; i++ )
    addItem( nl[i].name, nl[i].phone, nl[i].email, nl[i].address );

  document.getElementById('buildTime').innerText = ( (new Date()).valueOf() - buildStart ) + ' ms';
}

function startup() {
  var url = window.location.toString();
  url = url.replace( /script_js.html/, 'jscallback.php' );
  var elScript = document.createElement( 'script' );
  elScript.src = url;
  g_startTime = (new Date()).valueOf();
  document.body.appendChild( elScript );
}
</script>
...

The code in the ‘startup’ function, which is run in response to the load of the document creates a new <script> tag using document.createElement. It then sets the ‘src’ of the <script> to the ‘jscallback.php’ page. That page then generates a call to ‘addresses_callback’ which shows the contacts.

While this example is not specifically about efficiency it is an important tool to have in your web development kit. It's particularly key if you want to develop Javascript widgets that can be run from any web page.

Where To Go From Here

Another easy way to get some data throughput efficiency is to enable compression on your Apache web server. Shown below is the directive you need to use in your httpd.conf configuration file.

AddOutputFilterByType DEFLATE text/html text/plain text/xml text/javascript

This will compress not only your AJAX requests but all of the data send to the phone.

What we have tried to do in this article is present a set of tools that you can use to make your AJAX code efficient. That will certainly be an advantage on mobile devices, like iPhone. But it will also make your standard web interface more responsive as well. The real trick, however, is in acknowledging that, in Safari, you have a powerful client side platform that if used intelligently can be extremely bandwidth efficient.

iPhone is an exhilarating platform. Not only does it look and work great, but it's got access to the Internet wherever you are. This article is intended to help you build iPhone applications that run so fast people will want to use them wherever they are.

Updated: 2008-02-03