Monthly Archives: April 2012

Five JavaScript Tips for a Sleek User Experience

The eBay Motors engineering team took up an initiative to revisit some of the legacy JavaScript code shared across various pages, optimizing them to leverage the latest advancements in HTML5. Our main focus areas were user interactions and animations in which the old JavaScript code was lacking in performance and sturdiness. When we completed the exercise and demoed the upgraded experience to our product folks, the feedback we got was “SLEEK“. This post highlights five of those JavaScript techniques that we think made a difference.

1. requestAnimationFrame over setInterval: Using the new requestAnimationFrame API for building JavaScript-based animations is a more optimized and efficient approach when compared to the traditional timers and intervals. To quickly summarize, this API offloads the timer calculations for when to do the next UI/DOM style changes to the browser, rather than the developer deciding when to repaint the screen. We grepped our code base and replaced all applicable occurrences of setTimeout and setInterval with requestAnimationFrame; as a result, the animations were a lot smoother. This approach also saves battery life when content is viewed on mobile devices. As expected, the requestAnimationFrame API is not supported in all browsers, so we used polyfill, provided by Paul Irish, as shown below.

    // shim layer with setTimeout fallback
    window.requestAnimFrame = (function(w){
      return  w.requestAnimationFrame       ||
              w.webkitRequestAnimationFrame ||
              w.mozRequestAnimationFrame    ||
              w.oRequestAnimationFrame      ||
              w.msRequestAnimationFrame     ||
              function( callback ){
                w.setTimeout(callback, 1000 / 60);
              };
    })(window);

2. insertAdjacentHTML over innerHTML: The insertAdjacentHTML API is a fine-grained and optimized version of the super-popular innerHTML. Since we specify the insert position, insertAdjacentHTML does not re-parse the element it is being used on and avoids the extra step of serialization, making it much faster than direct innerHTML manipulation. This approach is very effective in scenarios where we keep appending markup to a page or module, such as for the daily deals feed and for endless scroll. A simple JSPerf test result shows that insertAdjacentHTML is 100% faster than innerHTML. Surprisingly, the browser support for this API has been there for a very long time (Firefox started supporting it in version 8), and the helper function is pretty straightforward:

    // helper function to append content for a given element
    var appendContent = function(){
        // Closure to hold the insertAdjacentHTML API support
        var insertAdjacentSupported = document.createElement('div').insertAdjacentHTML;

        return function(elem, content) {
            if(insertAdjacentSupported) {
                elem.insertAdjacentHTML('beforeend', content);
            } else {
                elem.innerHTML = elem.innerHTML + content;
            }
        };
    }();

3. if-else over try-catch: Try-catch blocks provide an efficient mechanism to handle exceptions and unforeseen runtime errors. However, they impose a performance penalty in JavaScript, especially when used in iterations or recursive functions. More details can be found at dev.opera.com and O’Reilly’s site. To address this issue, we scanned our code base, particularly looking for performance-critical functions. We found a few occurrences where a try-catch was inside a long-running iterator loop, and also where the probability of the thread entering the catch block was high. The try-catch was replaced with simple if-else conditions, which took care of all error handling, and the resulting code was much more efficient.

Using try-catch:

    // jsonResponse comes from some web service
    var i, l = jsonResponse.length, item, offer, binPrice, bidPrice;
    for(i = 0; i < l; i++) {
        item = jsonResponse[i];
        // Some code
        // ...
        try {
            offer = item.offer;
            binPrice = offer.bin;
            bidPrice = offer.bid;
        } catch(e) {
            offer = {err: "Offer not found"};
        }
        // Some more code
        // ...
    }

Using if-else:

    // jsonResponse comes from some web service
    var i, l = jsonResponse.length, item, offer, binPrice, bidPrice;
    for(i = 0; i < l; i++) {
        item = jsonResponse[i];
        // Some code
        // ...
        offer = item.offer;
        if(offer) {
            binPrice = offer.bin;
            bidPrice = offer.bid;
        } else {
            offer = {err: "Offer not found"};
        }
        // Some more code
        // ...
    }

4. XMLHttpRequestUpload over server polling: Using browsers to upload files (photos, PDFs, other documents, etc.) has become a very common use case, and one of our applications had this requirement. To keep users informed during the upload process, we wanted to show a real-time progress meter with accurate percentages. One (non-Flash) way of simulating this AJAX-based upload is to create a hidden iFrame and submit the main form, which holds the input file element targeted to the iFrame. Following the form submit, the server has to be polled periodically to retrieve the upload percentage for the progress meter. Not only is this approach a hack, but it also consumes huge amounts of server and client resources.
As a savior, the XMLHttpRequest object in modern browsers has the capability of uploading files (as byte streams) using the send method, and also has this amazing XMLHttpRequestUpload attribute. The upload attribute has a couple of associated events, the most important being the progress event. The progress event handler receives the total number of bytes to transfer and the number of bytes transferred so far, from the event's total and loaded fields. With this information, end users receive updates of real-time progress in the most efficient and sturdy way. Here is a quick preview of the API usage:

    var uploadAJAX = function(serverURL) {
        var xhr = new XMLHttpRequest(),
            fileElement = document.getElementById("file"),
            fileObj = fileElement[0], // For demo, taking only the first file in the file list
            progressMeter = getProgressMeterComponent(); // retrieve the progress meter UI component 

        xhr.upload.onprogress = function(e){
            if (e.lengthComputable){
                var percentComplete = Math.round(e.loaded / e.total * 100);
                progressMeter.update(percentComplete); // Updates the progress meter UI component with the given percentage
                }
        };    

        xhr.open("POST", serverURL, true);
        xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
        xhr.setRequestHeader("X-File-Name", encodeURIComponent(fileObj.name));
        xhr.setRequestHeader("Content-Type", "application/octet-stream");
        xhr.send(fileObj.file);
    };

We have open-sourced the HTML5 image uploader utility in github. For browsers that do not support this feature, the application falls back to the hidden iFrame approach.

5. CSS3 over JavaScript: The final optimization is to AVOID JavaScript as much as possible for animations (JavaScript is great but...) and to leverage the modern CSS3-based transitions. The new-age CSS comes with a ton of great animatable properties that can replace most of the basic animations currently implemented in JavaScript. The main advantages are ease of use, the browser doing most of the work, the leveraging of machine hardware if necessary, and above all an elegant and smooth visual touch to the transition (which is nearly impossible to achieve with JavaScript). We started changing our animations from simple tab switches (check it out) to complex 3D carousels (check out the beta version) - all with CSS, which netted a great user experience. For older browsers, we just stopped doing animations.

This entire re-engineering process also helped us develop a workflow for similar upgrades, thus enabling our code base to iterate at the same speed as do browser innovations. We can always hope that the need to use polyfills will be reduced in the near future.


Engineer @ eBay