To sync or not to sync

Posted by Jamis on March 12, 2005 @ 06:06 AM

David is making great strides with adding Ajax helpers to Rails. His efforts have been focused on synchronous requests, which have served him well with Ta-da Lists, and as he pushes ahead with Honey. Since he’s got the synchronous side nailed down pretty well, he’s asked me to take a look at the asynchronous side.

Update [13 Mar] I changed the URL of this article, because the ellipsis was wreaking havoc… sorry to those for whom that breaks existing links…

Synchronous XMLHttpRequests

Using synchronous requests with the XMLHttpRequest object has the benefit of allowing you to send a request and get a response back in a very easy-to-implement manner. With a bit of abstraction, you can write something like this:

  element.innerHTML = submit_request("/some/action?value=foo")

The drawback of synchronous requests is that, with every browser I’ve ever tried them on (including Safari, Firefox, Mozilla, and IE 6), sending a synchronous request causes the browser to “freeze” until the request returns. It just stops responding.

For very brief requests, this period of unresponsiveness is negligable. Consider Ta-da Lists—you add an item to a list, and it just blinks right up into the list. You never really notice the lag, unless you happen to be on a very slow network connection, or your network goes down at just the wrong moment.

That brings up a thorny problem: what if the network goes down at just the wrong moment? What if the user is on a very slow network connection?

It is easy to simulate this. In your web application, just put a sleep command in the appropriate action and have it go away for a few seconds. Observe how your application behaves now. Suddenly, it feels like the browser has crashed, or is hung. You press a button and get zero feedback that anything is being performed. Even your carefully designed mouse-over effects are silent. It is easy to imagine a user clicking the button over and over out of a sense of uncertainty—”is it doing what I asked it to do?”

But what is the alternative?

Asynchronous XMLHttpRequests

The XMLHttpRequest object also supports an asynchronous interface. This means you can send off a request and then do other things while the request is processing. It has the added benefit of not locking up your browser while the request is processing.

However, this added flexibility comes at a price: you can no longer simply send a request and expect the result immediately. You now have to register a callback, to be invoked when the request finishes, and have that callback do the rest of the work. (It is actually very reminiscent of call-with-current-continuation, if you’re into that kind of thing.)

Consider the following (again, very abstracted):

  submit_request("/some/action?value=foo", function(result) {
    element.innerHTML = result
  } )

I’m definitely not a Javascript guru—perhaps there is a way to make the two approaches (synchronous and asynchronous) have the same interface. But the point is that each requires a slightly different style of interface. With the synchronous approach, there is the implicit assumption that the request takes zero (or negligable) time. The asynchronous approach assumes a non-zero duration for the request, and this introduces other design decisions that ought to be made.

Most significantly, if your request is now assumed to take a noticable length of time, how do you indicate to the user that something is occurring? You don’t want them to get confused, wondering whether the button they just clicked actually did anything. You certainly don’t want them to just assume that it worked and go away, only to have the error message you tried to send them lost in transit.

Also, you certainly don’t want them to retry the action. Pressing a button over and over to elicit a response from the program is not generally a good idea, but that doesn’t prevent users from trying it.

UI Guidelines for Asynchronous Requests

Here are some things you might try to make the UI more responsive:

  1. Hide the button, or link, or whatever, that was used to initiate the request. If it’s not there, the user can’t click it.
  2. Replace the input with some text indicating that their request is being processed. An animated progress-bar can help, too, because it is a familiar indicator to most users.
  3. Optionally, if you already have all the information you need in the client, you can assume success and render the new text while the request is processing. If you do this, however, make sure to render it in a different style than “confirmed” text. This prevents users from thinking that their request succeeded, when the success or failure of the request is not yet known.
  4. Finally, when the request finishes, redisplay the buttons and links, show the input, hide the progress indicator, and basically take care of whatever else is needed to make the page look like it needs to look.

In Javascriptian pseudo-code:

  hide_input()
  show_progress_meter()
  submit_request("/some/action?value=foo", function(result) {
    hide_progress_meter()
    show_input()
    element.innerHTML = result
  } )

One last thing to beware of: you may need to set up sentinels to prevent incompatible operations from occuring simultaneously. If you have a button to (for instance) submit a new transaction to an account, and a link to delete a transaction, you may not want to allow both operations to occur simultaneously. You can either hide all other possible operations once an operation has been initiated, or you can set a sentinel variable and check for it in the other operations:

  var transaction_submission_in_progress = false
  var transaction_deletion_in_progress = false

  function submit_transaction() {
    if( transaction_deletion_in_progress ) return
    transaction_submission_in_progress = true
    hide_input()
    show_progress_meter()
    submit_request("/some/action?value=foo", function(result) {
      hide_progress_meter()
      show_input()
      element.innerHTML = result
      transaction_submission_in_progress = false
    } )
  }

  function delete_transaction() {
    if( transaction_submission_in_progress ) return
    transaction_deletion_in_progress = true
    ...
  }

In general, the asynchronous approach is certainly more complex, but it allows you much more freedom to build a more expressive and robust interface.

Posted in Essays and Rants

Be the first to leave a comment on this article. Tell us your thoughts using the form below!