Throttling Interaction with jQuery

Expanding on a previous post on $.ajax abort() about throttling user interaction, here are few plugins and techniques I’ve found to be very useful when developing jQuery heaving interfaces.

jquery-debounce
Smart JS Polling
blockUI plugin

Reviews after the jump…

Debounce and Throttle

With all of jQuery’s easy to use selectors it’s easy to overlook the reprocusions of a JS function on CPU usage. Class selecting (in old browsers without native support), chaining, tweening css properties, can all push browser CPU usage to the max when used in quick succession. Binding events to scrolls, mousemove’s, and keypresses highlight the need for some light throttling of functions that won’t limit user experience if delayed an extra 100ms.

In short, throttling will reduce the rate of a repeated event whereas debouncing will ensure that only one signal for a given event is fired within a fixed amount of time. (A much better and detailed explanation can be found here)

As an example, if someone is only able to type at 10 keys/second, what good would it be to fire events sooner than 100ms? You can see how things like autocompletes, keyevents, and scroll updates would require some sort sort of rate limiting.

jQuery Throttle & Debounce Plugin

My favorite plugin for this is jquery-debounce.

    $('input[name=suggest]').keyup($.debounce(onKeyUp, 300));
  


    $(window).resize($.throttle(doComplex?omputation, 300));
  


Smart Polling

In an ideal world, polling doesn’t exist and we are all using push models, but in reality the infastructure for a push service is not worth it in alot of cases and lightweight polling works just fine.

Prototype JS supports an Ajax.PeriodicalUpdater which works beautifully and supports things like decay functions and response logic.

For jQuery i’ve used Smart JS Polling which is a simple extension which gives you a wait time and a function to specify a backup/decay function.

Freeze/Block UI

There are alot of cases in a JS frontend where freezing the UI with a spinner would be less confusing (though more painful…) to the user that some processing needs to take place before they can continue clicking around and launching more actions.

For this, the jQuery blockUI plugin is easy to use and alleviates the clutter from having extra hidden divs and absolute this and that in your views.

Memcached 1.4 Released (binary protocol)

The in-memory cache server we all know and love released a new version with notable new features being the implementation of a binary protocol, a bunch of new stats commands for pulling CAS numbers and slab class eviction times, and a hodge-podge of performance improvements.

What’s interesting? Replication. The binary protocol allows for traditional replication of data between memcached servers in a cluster and therefore can increase the amount you can theoretically rely on the data stored in your cluster. With an increase in precaching and push type models for storing complex relationships (graphs, etc) memcached is maintaining its place at the top of the in-memory datastores with this bump in reliability.

Memcached on Google Code

Timeout Web Service Calls in Ruby 1.8 [tip]

Nothing new, but something I ran into recently with Ruby 1.8 and its conditionally working (more info) version of Timeout is a better implementation called SystemTimer.

While my use case was for opening TCP connections in daemon workers, it is even more critical in any blocking Rails code that uses external service calls (web service/external memcached/fulltext search/nosql cluster/etc). It’s easy enough to do and will save you from bottlenecks in the event one of you external properties or 3rd parties is not responsive.

Use it

require 'system_timer'
begin
  SystemTimer.timeout_after(2.seconds) do
    @sock = TCPSocket.new(host, port)
  end
rescue Timeout::Error
  @sock = nil
  raise Timeout::Error, "Timeout connecting to the server"
end

jQuery ajax quick tip: $.ajax abort()

Dealing with ajax has become so easy and high-level that it is pretty easy to forget about the details. A nice habit to get into when dealing with an ajax-heavy frontend is to manage the event listeners and connections that a user can initiate.

Something simple to remember is that you can’t rely on your requests to complete FIFO because your requests are getting routed across multiple processes/machines that may or may not be under load. Alot of users are rather click-happy (my mom still double-clicks links) which can lead to problems with content that was requested on the first click being loaded after the content from the most recent click.

In the past I have always included sequence numbers as request ID’s and then have the server include the request ID it is responding to in the JSON response. This gives the event handlers some context of the response it just received in relation to the most recent action the user has performed on the page.

This works well, but sometimes you just want to throw out any response that is not in response to the most recent request. This is where the abort() method is useful.

var current_conn;

function getSome() {
  if(current_request) {current_request.abort();}
  current_request = $.get('/events',
    { team : "Ramrod" }, 
    function(resp) { alert(resp); }
  );
}

If you need to, you could use the sequence number approach and create a response queue or other more complex solution. Here are a few neat ideas as well…

http://www.protofunc.com/scripts/jquery/ajaxManager/

Simple queue idea

Deep Linking Pagination with jQuery Address

With most sites today so heavily reliant on loads of asyncronous requests with javascript,
json/p, flash, etc. It can be quite frustrating when trying to navigate a JS image gallery or RSS reader
while also making sure never to accidently hit back/forward/refresh so that you do not lose your place in browsing.
Want to send a link to the specific photo you are browsing in a JS gallery? Good luck getting your friends to click
the link and then hit “Next” 32 times to see the specific photo.

Facebook does a great job at keeping all of their heavy javascript pages (such as the photo gallery) deep-linked so this is not
an issue.

In the past, the most popular library for deep-linking was swfAddress and was primarily used for Flash.
swfAddress now supports AJAX though the same team has also released a lightweight version for jQuery called jQuery Address.

To demostrate deep-linking with jQuery Address I put together a quick demo of deep-linked pagination of a Flickr photo gallery as an example. You should be able to use your back/forward/refresh/bookmark functions just fine.

View Demo

Source

  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
      <title>jQuery Address + Flickr Pagination Demo</title>
      <meta content="text/html; charset=utf-8" http-equiv="content-type" />

      <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
      <script type="text/javascript" src="jquery.address-1.0.min.js"></script>
      <script type="text/javascript" src="jquery.flickr-1.0-min.js"></script>

      <style type="text/css">
        div#flickr_photos ul { overflow: hidden; list-style-type: none; margin: 0px; padding: 0px; }
        div#flickr_photos ul li { float: left; }
        div#flickr_pagination a, div#flickr_pagination span { margin-right: 10px; }
      </style>

      <script type="text/javascript">
        /*<![CDATA[*/
        var current_page = 1;

        function refreshPagination(ele) {
          $("#flickr_loader").remove();

          var current_page = parseInt($(ele).find("input:eq(0)").val());
          var total_pages = parseInt($(ele).find("input:eq(1)").val());

          if(current_page <= 1) {
            $("<span>").html("Previous").appendTo("#flickr_pagination");
            $("<a>").attr("href", "#").html("Next").bind("click", nextPage).appendTo("#flickr_pagination");
          } else if (current_page >= total_pages) {
            $("<a>").attr("href", "#").html("Previous").bind("click", previousPage).appendTo("#flickr_pagination");
            $("<span>").html("Next").appendTo("#flickr_pagination");
          } else {
            $("<a>").attr("href", "#").html("Previous").bind("click", previousPage).appendTo("#flickr_pagination");
            $("<a>").attr("href", "#").html("Next").bind("click", nextPage).appendTo("#flickr_pagination"); 
          }
        }

        function previousPage() {
          $.address.value("?page="+(current_page-1));
          return false;
        }

        function nextPage() {
          $.address.value("?page="+(current_page+1));
          return false;
        }

        function getPhotos(page) {
          $.address.title($.address.title().split(' | ')[0] + ' | page '+page);

          $("#flickr_photos").html($("<img/>").attr("src", "spinner.gif").attr("id", "flickr_loader"));
          $("#flickr_pagination").html("");

          $("#flickr_photos").flickr({
      			api_key: "f28804be7a09c5845676349c7e47d636",     
      			per_page: 5,
      			page: page,
      			type: "photoset",
      			photoset_id: "72157600037079977",
      			callback: refreshPagination
      		});
        }

        function addressChangeHandler(evt) {
          current_page = $.address.parameter("page");
          if(current_page) {
            current_page = parseInt(current_page);
          } else {
            current_page = 1;
          }

          getPhotos(current_page);
        }

        $(document).ready(function(){
          // Setup jQuery Address Listener
          $.address.change(addressChangeHandler);
        });
        /*]]>*/
      </script>
    </head>
    <body>
      <div id="flickr_photos"></div>
      <div id="flickr_pagination"></div>
    </body>
  </html>