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>
|
 | |
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
And Figure 1 shows the page as it's seen in my browser.
Figure 1. The 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 antipat1a_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>
|
You can see the page in action in my browser in Figure 2.
Figure 2. 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 antipat1b_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.
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;
}
...
|
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 antipat2_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.
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>
|
 | |
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
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 antipat3_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>
|
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.
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 antipat4_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.
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 antipat5_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.
Conclusion
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
Rate this page
|