Skip to main content

skip to main content

developerWorks  >  XML | Web development  >

Ajax and XML: Five Ajax anti-patterns

Avoid the common pitfalls of Ajax code

developerWorks
Document options

Document options requiring JavaScript are not displayed

Discuss


Rate this page

Help us improve this content


Level: Intermediate

Jack D Herrington (jherr@pobox.com), Senior Software Engineer, Leverage Software Inc.

20 Mar 2007

You can learn a lot about how to do things correctly by understanding how things are done incorrectly. Certainly, there's a right way and a wrong way to write Asynchronous JavaScript™ + XML (Ajax) applications. This article discusses some common coding practices you will want to avoid.

If people did everything right the first time, the world would be entirely different. So it is with Ajax. I've done a lot of coding, writing, talking, and supporting of Ajax developers, including myself. And through that I've learned a lot about how Ajax is done right and how it's done wrong. In my last article, Five common Ajax patterns: Helpful Ajax design patterns you can use today, I presented five patterns for writing Ajax applications correctly. In this article, I present five anti-patterns that I often see in Ajax code.

What is an anti-pattern, you might ask? An anti-pattern is an application design flaw that's seen often enough to be an issue everyone should watch out for. I'm talking high level here. Syntax errors and linker problems need not apply.

Most developers have heard of a good example of an anti-pattern: The improper use of Structured Query Language (SQL) libraries that result in SQL injection attacks on Web sites. That anti-pattern has cost companies lost revenue, exposed customer records, and unfortunately is achievable in every programming language. So, it's worth understanding how and why it happens and how to avoid it.

So it is with these Ajax anti-patterns. Now, I'm not saying that they can cost companies billions in lost revenues. But they can crash servers or provide poor customer experience, and that can be both frustrating and expensive.

You can learn a lot if you understand what can go wrong. Too often, people think that Ajax is just a way to fetch XML from the server after a page has loaded. That's a very limited view, and it can cause performance problems in applications if it's misapplied. In this article, I present both why it's wrong and how to fix it.

Polling on a timer when you don't need to

Many of the Ajax issues I see have to do with misuse of the timer functionality built into the JavaScript language. The key method is window.setInterval(). Whenever you see that method, it should set off a little alarm in your mind; why is this person using a timer? Certainly, timers have their purpose -- animations, for example.

The window.setInterval() method tells the page to call back a particular function on a particular interval -- say, every second. Most browsers talk a good game when it comes to these timers, but they rarely deliver, primarily because the JavaScript language is single threaded. If you ask for a second, you might get the callback at 1 second or 1.2 seconds or 9 seconds or any other time.

One place a timer is certainly not necessary is to watch for an Ajax request to finish. Take the example in Listing 1.


Listing 1. Antipat1a_polling.html
	
<html><script>
var req = null;

function loadUrl( url ) {
  if(window.XMLHttpRequest) {
    try { req = new XMLHttpRequest();
    } catch(e) { req = false; }
  } else if(window.ActiveXObject) {
    try { req = new ActiveXObject('Msxml2.XMLHTTP');
    } catch(e) {
    try { req = new ActiveXObject('Microsoft.XMLHTTP');
    } catch(e) { req = false; }
  } }
  if(req) {
    req.open('GET', url, true);
    req.send('');
  }
}

window.setInterval( function watchReq() {
    if ( req != null && req.readyState == 4 && req.status == 200 ) {
      var dobj = document.getElementById( 'htmlDiv' );
      dobj.innerHTML = req.responseText;
      req = null;
   }
 }, 1000 );

var url = window.location.toString();
url = url.replace( /antipat1a_polling.html/, 'antipat1_content.html' );
loadUrl( url );
</script><body>
Dynamic content is shown between here:<br/>
<div id="htmlDiv" style="border:1px solid black;padding:10px;">
</div>And here.</body></html>

To see what this looks like in a live environment, view this online version of Open link to view antipat1a_polling.html in new windowantipat1a_polling.html.

Everything looks pretty good until you get to the setInterval call. This call sets a timer that will watch the status of the request, and then sets the content of the page from the downloaded material.

I'll show a better solution to the problem of hoe to figure out when a request is complete shortly. In the meantime, Listing 2 shows the file the page is requesting.


Listing 2. Antipat1_content.html
	
<b>Hello there</b>

To see what this looks like in a live environment, view this online version of Open link to view antipat1_content.html in new windowantipat1_content.html.

And Figure 1 shows the page as it's seen in my browser.


Figure 1. The content placed in the HTML document
Content placed in the HTML document

So, you might ask yourself, "Well, this works, right? Why fix it if it isn't broken?" It is actually broken because it's very slow. With the timer set to a one-second interval, by the time it goes off, the request is well past complete. So, first the page comes up with an empty box, then you wait a second, and boom: The content comes in. Ugh.

What's the solution? Ajax is asynchronous by its very nature. Don't you need a polling loop to see when a request is finished?

It turns out, not so much. As I show in Listing 3, all the XMLHTTPRequest object provides is a callback mechanism called onreadystatechange. (What a beautiful name, so reminiscent of VAX PDP/11s).


Listing 3. Antipat1a_fixed.html
	
<html><script>
var req = null;
function processReqChange() {
  if (req.readyState == 4 && req.status == 200 ) {
    var dobj = document.getElementById( 'htmlDiv' );
    dobj.innerHTML = req.responseText;
  }
}

function loadUrl( url ) {
  ...
  if(req) {
    req.onreadystatechange = processReqChange;
    req.open('GET', url, true);
    req.send('');
  }
}

var url = window.location.toString();
url = url.replace( /antipat1a_fixed.html/, 'antipat1_content.html' );
loadUrl( url );
</script>
...

To see what this looks like in a live environment, view this online version of Open link to view antipat1a_fixed.html in new windowantipat1a_fixed.html.

This new code just looks to see whether the request object has changed in response to this onreadystatechange callback. Then, it updates the page when it finishes.

The result is a lightning-fast page load. The page comes up, and almost immediately the box is filled in with the new content. Why? Because as soon as the request is complete, the code is called, and I fill in the page. No need to mess around with silly timers.

Another variant of the polling anti-pattern is when a page bounces a request off the server over and over again, even if the request hasn't changed. Take the search page shown in Listing 4.


Listing 4. Antipat1b_polling.html
	
<html><script>
var req = null;
function processReqChange() {
  if (req.readyState == 4 && req.status == 200 ) {
    var dobj = document.getElementById( 'htmlDiv' );
    dobj.innerHTML = req.responseText;
  }
}

function loadUrl( url ) {
  ...
}

window.setInterval( function watchSearch() {
  var url = window.location.toString();
  var searchUrl = 'antipat1_content.html?s='+searchText.value;
  url = url.replace( /antipat1b_polling.html/, searchUrl );
  loadUrl( url );
 }, 1000 );

</script><body><form>
Search <input id="searchText" type="text">:<br/>
<div id="htmlDiv" style="border:1px solid black;padding:10px;">
</div></form></body></html>

To see what this looks like in a live environment, view this online version of Open link to view antipat1b_polling.html in new windowantipat1b_polling.html.

You can see the page in action in my browser in Figure 2.


Figure 2. The search area with the dynamic response area
The search area with the dynamic response area

How pretty. From the look of it, this page makes a lot of sense. When I change the search text, the result area changes based on the new criteria (well, not really, but if I had a real search engine behind the request, it would.)

The problem is that the JavaScript code is using window.setInterval to keep making the request over and over, even when the content of the search field hasn't changed. That eats up network bandwidth, and it eats up server time. On popular sites, that can be a deadly combination.

The solution is to use the event callbacks on the search box, as shown in Listing 5.


Listing 5. Antipat1b_fixed.html
	
<html><script>
var req = null;
function processReqChange() { ... }
function loadUrl( url ) { ...  }

var seachTimer = null;
function runSearch()
{
  if ( seachTimer != null )
    window.clearTimeout( seachTimer );
  seachTimer = window.setTimeout( function watchSearch() {
    var url = window.location.toString();
    var searchUrl = 'antipat1_content.html?s='+searchText.value;
    url = url.replace( /antipat1b_fixed.html/, searchUrl );
    loadUrl( url );
    seachTimer = null;
   }, 1000 );
}
</script><body><form>
Search <input id="searchText" type="text" onkeyup="runSearch()">:<br/>
<div id="htmlDiv" style="border:1px solid black;padding:10px;">
</div></form></body></html>

To see what this looks like in a live environment, view this online version of Open link to view antipat1b_fixed.html in new windowantipat1b_fixed.html.

Here, I hooked the runSearch() function onto the onkeyup() method of the search box. That way, I get called back when the user types something into the search box.

What runSearch() does is pretty nifty. It sets a single timeout for a second way that will call the server and actually run the search. And it clears that timeout if it hasn't yet elapsed before setting it. Why? Because that allows for the user to type a lot of text; then, a second after the user presses the last key, the search will run. That way, the user isn't bothered by a continuously flickering display.



Back to top


Not inspecting the return results in the callback

Many Ajax anti-patterns spring from misunderstanding the mechanism of the XMLHTTPRequest object. One that I often see is when users don't inspect the readyState or status fields of the object in the callback. Have a look at Listing 6 to see what I mean.


Listing 6. Antipat2_nocheck.html
	
<html><script>
var req = null;
function processReqChange() {
  var dobj = document.getElementById( 'htmlDiv' );
  dobj.innerHTML = req.responseText;
}
...

To see what this looks like in a live environment, view this online version of Open link to view antipat2_nocheck.html in new windowantipat2_nocheck.html.

Everything looks okay. And on small requests, and on some browsers, it's probably fine. But many requests are large enough to call several calls to the onreadystatechange handler before they finish. So, your callback might be working with incomplete data.

The right way to do it is shown in Listing 7.


Listing 7. Antipat2_fixed.html


<html><script>
var req = null;
function processReqChange() {
  if (req.readyState == 4 && req.status == 200 ) {
    var dobj = document.getElementById( 'htmlDiv' );
    dobj.innerHTML = req.responseText;
  }
}
...

To see what this looks like in a live environment, view this online version of Open link to view antipat2_fixed.html in new windowantipat2_fixed.html.

It's not much more code, and it works on every browser.

I have noticed in Windows® Internet Explorer® 7 that this problem is more acute than on other browsers. Internet Explorer 7 calls back to onreadystatechange a lot -- and I mean a lot, even on small requests. So, it's worth having your handlers written properly.



Back to top


Passing complex XML when HTML would be better

At one company I worked with, the talk was all about putting "intelligence at the edge of the network." Yet another goofy phrase for a fairly simple idea, use the smarts of the browser to do work on the desktop instead of doing it all on the server.

But putting a lot of intelligence in your pages means putting a lot of JavaScript code there. And that has a big downside: browser compatibility. You really should test every non-trivial line of JavaScript code on every popular browser -- or at least, the browsers that your customers are likely to use. And that means work. Take the example of complex Ajax code in Listing 8.


Listing 8. Antipat3_complex.html
	
<html><head><script>
var req = null;
function processReqChange() {
  if (req.readyState == 4 && req.status == 200 && req.responseXML ) {
    var dtable = document.getElementById( 'dataBody' );
    var nl = req.responseXML.getElementsByTagName( 'movie' );
    for( var i = 0; i < nl.length; i++ ) {
      var nli = nl.item( i );
      var elYear = nli.getElementsByTagName( 'year' );
      var year = elYear.item(0).firstChild.nodeValue;
      var elTitle = nli.getElementsByTagName( 'title' );
      var title = elTitle.item(0).firstChild.nodeValue;

      var elTr = dtable.insertRow( -1 );

      var elYearTd = elTr.insertCell( -1 );
      elYearTd.innerHTML = year;

      var elTitleTd = elTr.insertCell( -1 );
      elTitleTd.innerHTML = title;
} } }

function loadXMLDoc( url ) {
  if(window.XMLHttpRequest) {
    try { req = new XMLHttpRequest();
    } catch(e) { req = false; }
  } else if(window.ActiveXObject) {
    try { req = new ActiveXObject('Msxml2.XMLHTTP');
    } catch(e) {
    try { req = new ActiveXObject('Microsoft.XMLHTTP');
    } catch(e) { req = false; }
  } }
  if(req) {
    req.onreadystatechange = processReqChange;
    req.open('GET', url, true);
    req.send('');
  }
}

var url = window.location.toString();
url = url.replace( /antipat3_complex.html/, 'antipat3_data.xml' );
loadXMLDoc( url );
</script></head><body>
<table cellspacing="0" cellpadding="3" width="100%"><tbody id="dataBody">
<tr>
  <th width="20%">Year</th>
  <th width="80%">Title</th>
</tr>
</tbody></table></body></html>

To see what this looks like in a live environment, view this online version of Open link to view antipat3_complex.html in new windowantipat3_complex.html.

This code reads data from the XML file shown in Listing 9 and formats it into a table.


Listing 9. Antipat3_data.xml
	
<movies>
  <movie>
    <year>1993</year>
    <title>Jurassic Park</title>
  </movie>
  <movie>
    <year>1997</year>
    <title>The Lost World: Jurassic Park</title>
  </movie>
  <movie>
    <year>2001</year>
    <title>Jurassic Park III</title>
  </movie>
</movies>

You can see the result in Figure 3.


Figure 3. The complex movie listings page
Complex movie listings page

This is not bad code at all. It's just a lot of code to perform what is actually a relatively simple task. The resulting page isn't complex at all. It can't be sorted or searched on the client side. In fact, there is almost no reason to go through this complex conversion between XML and HTML.

Wouldn't it be simpler to have the server return HTML instead of XML, as in Listing 10?


Listing 10. Antipat3_fixed.html
	
<html><script>
var req = null;
function processReqChange() {
  if (req.readyState == 4 && req.status == 200 ) {
    var dobj = document.getElementById( 'tableDiv' );
    dobj.innerHTML = req.responseText;
  }
}

function loadUrl( url ) { ... }

var url = window.location.toString();
url = url.replace( /antipat3_fixed.html/, 'antipat3_content.html' );
loadUrl( url );
</script><body><div id="tableDiv"></div></body></html>

To see what this looks like in a live environment, view this online version of Open link to view antipat3_fixed.html in new windowantipat3_fixed.html.

And indeed, it is simpler. All the complex table row and cell creation code is replaced with a single set of the innerHTML of a <div> tag on the page. Voilà!

The HTML returned from the server is shown in Listing 11.


Listing 11. Antipat3_content.html
	
<table cellspacing="0" cellpadding="3" width="100%">
<tbody id="dataBody">
<tr>
  <th width="20%">Year</th>
  <th width="80%">Title</th>
</tr>
<tr>
  <td>1993</td>
  <td>Jurassic Park</td>
</tr>
<tr>
  <td>1997</td>
  <td>The Lost World: Jurassic Park</td>
</tr>
<tr>
  <td>2001</td>
  <td>Jurassic Park III</td>
</tr>
</tbody>
</table>

To see what this looks like in a live environment, view this online version of Open link to view antipat3_content.html in new windowantipat3_content.html.

As with everything, the choice between whether to process on the server or on the client depends on the demands of the job. In this case, the job was relatively simple: Put up a table of movies. If the job were more complex -- perhaps with sorting, searching, adding or deleting, or dynamic interaction in which clicking a movie brings up more information -- then I can see going with more complex code on the client. In fact, I'll demonstrate sorting on the client at the end of this article, where I talk about the counter-argument of putting too great a load on the server.

Perhaps the perfect example of all of this is Google Maps. Google Maps does an elegant job of mixing rich-client side code with an intelligent mapping engine on the server side. I use that service as an example of how to determine what processing to do where.



Back to top


Passing XML when you should pass JavaScript code

With all the hype about having Web browsers read XML data sources and dynamically render them, you might think it was the only method available. However, you would be wrong, because very smart engineers have used Ajax transport technology to send JavaScript code instead of XML. Take the version of the movie table example shown in Listing 12.


Listing 12. Antipat4_fixed.html
	
<html><head><script>
var req = null;
function processReqChange() {
  if (req.readyState == 4 && req.status == 200 ) {
    var dtable = document.getElementById( 'dataBody' );
    var movies = eval( req.responseText );
    for( var i = 0; i < movies.length; i++ ) {
      var elTr = dtable.insertRow( -1 );
      var elYearTd = elTr.insertCell( -1 );
      elYearTd.innerHTML = movies[i].year;
      var elTitleTd = elTr.insertCell( -1 );
      elTitleTd.innerHTML = movies[i].name;
} } }

function loadXMLDoc( url ) { ... }

var url = window.location.toString();
url = url.replace( /antipat4_fixed.html/, 'antipat4_data.js' );
loadXMLDoc( url );
</script></head><body>
<table cellspacing="0" cellpadding="3" width="100%">
<tbody id="dataBody"><tr>
  <th width="20%">Year</th>
  <th width="80%">Title</th>
</tr></tbody></table></body></html>

To see what this looks like in a live environment, view this online version of Open link to view antipat4_fixed.html in new windowantipat4_fixed.html.

Instead of reading XML from the server, it reads JavaScript code. The code then uses the eval() function on the JavaScript code to get the data, which it can then quickly use to build the table.

The JavaScript data is shown in Listing 13.


Listing 13. Antipat4_data.js
	
[ { year: 1993, name: 'Jurassic Park' },
  { year: 1997, name: 'The Lost World: Jurassic Park' },
  { year: 2001, name: 'Jurassic Park III' } ]

This functionality requires that you get the server to talk in the JavaScript language. But that's not usually a big deal. Most of the popular Web languages already support JavaScript Object Notation (JSON) output.

The benefits are clear. In this example, there was a 52% savings in the size of the data blob downloaded to the client by going with the JavaScript language. There's a performance increase, as well. It was 9% faster to read the JavaScript version. While 9% might not seem like a lot, just remember that this was a fairly rudimentary example. Larger blocks of data or more complex structures will require more XML parsing code, while the JavaScript code will remain unchanged.



Back to top


Doing too much on the server

A counter-argument to doing to little on the server is doing too much on the server. As I mentioned before, it's a balancing act. But I would like to show how to perform sorting of the movie table on the client as a demonstration of how to offload work from the server.

This sortable movie table is shown in Listing 14.


Listing 14. Antipat5_sort.html
	
<html><head><script>
var req = null;
var movies = null;
function processReqChange() {
  if (req.readyState == 4 && req.status == 200 ) {
    movies = eval( req.responseText );
    runSort( 'year' );
} }

function runSort( key )
{
  if ( key == 'name' )
    movies.sort( function( a, b ) {
      if ( a.name < b.name ) return -1;
      if ( a.name > b.name ) return 1;
      return 0;
    } );
  else
    movies.sort( function( a, b ) {
      if ( a.year < b.year ) return -1;
      if ( a.year > b.year ) return 1;
      return 0;
    } );
  var dtable = document.getElementById( 'dataBody' );
  while( dtable.rows.length > 1 ) dtable.deleteRow( 1 );
  for( var i = 0; i < movies.length; i++ ) {
    var elTr = dtable.insertRow( -1 );
    var elYearTd = elTr.insertCell( -1 );
    elYearTd.innerHTML = movies[i].year;
    var elTitleTd = elTr.insertCell( -1 );
    elTitleTd.innerHTML = movies[i].name;
  }
}

function loadXMLDoc( url ) { ... }

var url = window.location.toString();
url = url.replace( /antipat5_sort.html/, 'antipat4_data.js' );
loadXMLDoc( url );
</script></head><body>
<table cellspacing="0" cellpadding="3" width="100%">
<tbody id="dataBody"><tr>
  <th width="20%"><a href="javascript: void runSort('year')">Year</a></th>
  <th width="80%"><a href="javascript: void runSort('name')">Title</a></th>
</tr></tbody></table></body></html>

To see what this looks like in a live environment, view this online version of Open link to view antipat5_sort.html in new windowantipat5_sort.html.

This is a fairly simple example. It wouldn't work for particularly long lists of movies that would likely be shown in pages. But it does demonstrate that it's easy to put together a table that sorts quickly, without a page refresh and without bothering the server to do the hard, uninteresting work of the sort.



Back to top


Conclusion

Share this...

digg Digg this story
del.icio.us Post to del.icio.us
Slashdot Slashdot it!

I've written many articles on Ajax and done a lot of Ajax work. I moderate the Ajax forum on IBM developerWorks. So, I know a thing or two about Ajax and how it's used and misused. One of the things I see most often is developers underestimating the complexity of Ajax by thinking that it's just about sending some XML, JavaScript, or HTML code to the browser. I look at the Ajax platform as the entire browser -- in fact, the entire set of popular browsers, because you have to know the quirks of all of them.

What it all comes down to is this: There's a lot about Ajax to learn, and there are a lot of mistakes to make along the way. Hopefully, this article will help you avoid some of the pitfalls or get out of them when you fall in. Either way, while there is a lot you can learn from your successes, there is often more that you can learn from your mistakes.



Resources

Learn
  • developerWorks XML zone: Learn all about XML at the developerWorks XML zone.

  • JSON home page: Visit the JSON page, a reference for passing JavaScript Object Notation data from the server to the client.

  • ECMA International site: Find the definitions for JavaScript, actually the language known as ECMAScript.

  • AjaxPatterns: Explore this site dedicated to the topic.

  • Design Patterns (Erich Gamma, et al., Addison-Wesley, 1995): Read the seminal book on the use of patterns in software engineering.

  • Ajax Design Patterns (Michael Mahemoff, O’Reilly, 2006): Read this book, a new source specifically covering design patterns for Ajax.

  • IBM XML certification: Find out how you can become an IBM-Certified Developer in XML and related technologies.

  • XML technical library: See the developerWorks XML Zone for a wide range of technical articles and tips, tutorials, standards, and IBM Redbooks.

  • developerWorks technical events and webcasts: Stay current with technology in these sessions.

  • Ajaxian: With this great resource, keep up with developments in Ajax and the front-end widgets that use it.


Discuss


About the author

Jack D. Herrington is a senior software engineer with more than 20 years of experience. He's the author of three books: Code Generation in Action, Podcasting Hacks, and PHP Hacks. He has also written more than 30 articles. You can reach Jack at jherr@pobox.com.




Rate this page


Please take a moment to complete this form to help us better serve you.



YesNoDon't know
 


 


12345
Not
useful
Extremely
useful
 


Back to top