HTML5 Application Optimization

This post is also available in: Russian

This article provides tips on how to optimize the client part of large-scale Web projects (most of the recommendations also apply to small projects, however). Just within a few years, Web sites have made a giant leap, having evolved from simple static pages to complex applications. The website frontends have as dramatically grown in size and logic, and now they demand no less attention in the context of optimization than backends, as the frontends are responsible for interacting with the users.

The frontend of a Web project consists of 3 main parts: HTML, CSS, JavaScript. Below we are going to present the ways to optimize each of these application components, providing some general tips to speed up the frontend.

General Tips

Reduce the Number of HTTP Requests

Remember that each time an HTTP request is made, the application has to wait until a connection between the frontend and the backend is established. The Figure below outlines timing intervals of querying google.com:

As you can see, data fetching took less than 32%. All the other time it took to connect to the server and prepare the outbound data on the server. Therefore, frequent requests containing small amounts of data can result in dramatic performance degradation. By reducing the number of HTTP requests, we can minimize connect time and wait time.

Combine Your Content

For the above reasons, combine your scripts and CSS styles within common files. As far as images are concerned, arrange them in sprites and do not forget to optimize them also. When placing images in sprites, try to expand your sprite horizontally rather than vertically: in this case, the file size will be lower.

There is a variety of code minification tools. The most popular of them is YUI Compressor (http://developer.yahoo.com/yui/compressor/). Also notable is Google Closure Compiler (https://developers.google.com/closure/compiler/). This tool goes beyond of simply minificating the code. It analyzes the code, optimizes and discards redundant chunks in order to boost the performance. But please also keep in mind the downside: minificated code and compiled code all the more may show erroneous and be hard to debug. Therefore, make sure to test your application properly. To automate this process, please take advantage of Unit tests that we are going to discuss in our post to come.

Do Not Scale the Images

It is inefficient to load large images and then scale them down. Prepare files of all sizes you’ll need.

Use GET Wherever Possible.

The reason behind giving precedence to GET over POST is that POST sends request headers first, and only then the data. In GET, everything is sent in a single TCP packet.

Manage Caching Time

Make sure to manage content caching in your projects. Each year, Web applications design is becoming ever more sophisticated. This translates to an increased number of scripts, styles and pages. Do not make the user continuously download the same content. For static content, use the "Expires" header, for dynamic content, use "Cache-Control".

Store Your CSS and JS in External Files.

Make sure to move all your scripts and styles to external files: in such a case, the user’s browser will cache them so as not to load the same information each time.

Reduce and Compress Your Styles and Scripts.

For all of your projects, follow the practice to minificate your CSS and JS scripts. This procedure will remove comments and extra white spaces from your code, reducing the size of downloaded files. Further compression will also benefit the data size.

Distribute Your Content Across Subdomains

If you put your scripts to sub1.site.com, and styles to sub2.site.com, the browser will download them in parallel. Typically, the browsers can support up to 10 concurrent downloads, so you can split images by subdomains and increase their download intensity. Also, remember of cookies sent with requests. Using subdomains, we can also get rid of unneeded cookies.

Avoid Redirects in Your AJAX Requests

Try not to use redirects as they result in longer wait times for the user. Remember that very often a redirect is made in the absence of a closing slash at the end of the URL. For instance, a request to www.site.com/directory will redirect the browser to www.site.com/directory/.

Pre-loading of components is cool!

Many components (such as images and styles) require pre-loading. This will increase the speed of their display to the user.

Post-loading is also a pretty nice thing.

Some components should really be loaded among the latest, i.e., the js scripts. The fact is that, when the browser encounters the <script> tag on the page, it stops rendering the content until the script is executed.

Also, it is better to postpone rendering of page elements that require complex calculations from the server, or just have a long delay in request. It is most optimal way to load such elements is to use AJAX requests, while the user is alerted to wait until loading is complete.

Separate versions for mobile devices

Please keep in mind that some users land to your site from their phones and tablets. Computing capacities of such devices are inferior to personal computers. So please give your time to developing of a light version of your project for such users.

Accelerate Your HTML Markup

Reduce the number of elements in the DOM tree and optimize them

The less elements you have, the easier it would be for browsers to render them. Beware of using flexible width tables as drawing them is quite resource-intensive.

Close Your Tags

Unclosed tags may not just result in layout errors, but also, in modern browsers, increase the rendering times as the browser tries to parse the page and close the tags properly.

A Few Words on CSS

Do not duplicate style properties

If you have multiple elements with a similar description (for example, with a blue 1-pixel border), then it is not necessary to describe the border for each of them. Assign to all of them an additional class to draw the border. For example:

.bordered {
    border: 1px solid blue;
}

Use a short form of styles

Many CSS properties have both long and short syntax (e.g., margin, padding, border, background). Use the short form to describe your styles. This will reduce the size of your CSS files. For example:

.style { 	/* Bad */
	margin-top: 5px;
        margin-left: 20px;
}

.style {		/* Good */
	margin: 5px 0 0 20px;
}

Abstain from Filter, Expressions

Try not to use these properties in your styles. They generate a high load on your browser, resulting in delays in page rendering.

Animation

With modern Web technologies, you can add animation to your page. 2 basic technologies offering animation are JavaScript and CSS3 Transitions.

For JS-animation, popular libraries (including JQuery) provide for easy management of the object movement and transformation. However, even the simplest JS animation consists in choosing a set of successive changes in the object display status (i.e., its CSS styles). Every time such a change occurs, the DOM tree is rebuilt, thus creating a serious load on your browser. And if you have several parallel transforms, you can forget of the smoothness altogether. But mobile devices virtually can not cope with such tasks, and instead of captive movements they make a slideshow effect.

CSS animation is a much less resource-savvy process. This article provides an example of CSS performance benefits. However, on embed devices the smoothness may still not be ideal. So please consider whether you need animated transforms on the mobile version of your site.

Accelerate Animation

Animation speed and smoothness depends on the availability of a GPU and a browser with hardware acceleration enabled. Here is the a of browsers supporting graphics accelerator:

  • Google Chrome 13
  • Mozilla Firefox 4
  • Microsoft Internet Explorer 9
  • Opera 11
  • Apple Safari 5

Measure Animation Speed

For FPS measurement, we recommend a small JS-library, Stats.js. It displays a small widget reflecting animation speed on your site. Also, you can use a bookmarklet to add such a monitor to any site. To do this, bookmark the below link, and then navigate to your site and follow it.

FPS Monitor

Tools to measure the site load speed

To analyze your site, you may use a variety of tools. Here you can find an extensive list of such profilers.

Also, you can measure the speed of page loading and speed of building the DOM. For this purpose, use the window.onload and $(document).ready() events. The window.onload event will trigger when all the content, images, and scripts have been loaded. $(document).ready triggers as soon as the DOM is built. However, there is no way to measure the net rendering rate.

Example:

beforeload = (new Date()).getTime();
function pageLoadingTime() {
    afterload = (new Date()).getTime();
    secondes = (afterload-beforeload)/1000;
    console.log('Your Page Load took  ' + secondes + ' seconde(s).');
}

function domLoadingTime() {
    afterload = (new Date()).getTime();
    secondes = (afterload-beforeload)/1000;
    console.log('Your DOM Load took  ' + secondes + ' seconde(s).');
}

window.onload = pageLoadingTime;
$(document).ready(domLoadingTime);

JavaScript Optimization

Location of scripts on the page

JS attachment is more important than you might imagine. The peculiarity lies in the fact that having met the <script> tag, the browser first loads the file with the code and then executes it. For the time of execution, the browser first stops all the other activities, be it content download or rendering of the page.

To optimize viewing of an application, you have to control the script loading process. They should be located at the end of the page so that the main rendering could have already started and the user could see the interface elements first. To run scripts after loading of page elements, use the window.onload event.

Code optimization

Data access

In JavaScript, the functions always have access to objects outside of them. For example:

var name = “Some Name”;
var someFunc = function() {
	return name;
}
someFunc(); // Returns “Some Name”;

However, access to external data is slower than to the internal data. Therefore, if the function makes multiple calls to external objects, you should better create a local copy of the external entity to run further actions on it. When the function is complete, you just have to assign the internal object data to the external object.

Example:

var arr1 = [],
    arr3 = [];

var func1 = function() {
    for (var i = 0; i<100000; i++) {
        arr1.push(i);
    }
}

var func2 = function() {
    var arr2 = [];

    for (var i = 0; i<100000; i++) {
        arr2.push(i);
    }
}

var func3 = function() {
    var arr4 = arr3;

    for (var i = 0; i<100000; i++) {
        arr4.push(i);
    } 

    arr3 = arr4;
}

var startTime = (new Date()).getTime();
func1();
console.log('Outer Arr push time: ' + ((new Date()).getTime()- startTime));

startTime = (new Date()).getTime();
func2();
console.log('Inner Arr push time: ' + ((new Date()).getTime()- startTime));

startTime = (new Date()).getTime();
func3();
console.log('Combo Arr push time: ' + ((new Date()).getTime()- startTime));

Result of code execution:

Outer Arr push time: 65
Inner Arr push time: 11
Combo Arr push time: 12

As you can see, the speed of adding an element to an external array is 5.5 times slower compared to a local one. At the same time, the time needed to assign the content of external array to the internal array and back takes only 1 ms and has but a little effect on the code performance.

Memorization

If you have a procedure performing complex operations, and such procedure is run repeatedly, so to eliminate redundancy, save the results of running the procedure. An example of such a function is computing of factorials:

var result = 1,
    resultArr = [0, 1];

var factorial = function(num) {
    if (resultArr[num]) {
        return resultArr[num];
    } else {
        var k = resultArr.length - 1;
        result = resultArr[k];

        for (var i = k; i <= num; i++) {
            console.log(i);
            result = result * i;
            resultArr[i] = result ;
        }

        return resultArr;
    }
}

Now every run of the factorial function immediately calculates smaller factorials. In case you need a higher factorial, its would be calculated started with the maximum previously calculated factorial rather than with 1.

Optimize Conditional Statements

If..else statements are often used to build the application logic. If there are many execution conditions, then the most likely of them should be placed as high as possible in the code, so that the least probable conditions are not checked most of the time.

DOM Operations

Adding New Elements

DOM manipulations are very browser-intensive. Let’s, say, we have a procedure adding multiple <span> elements in a cycle. The browser will rebuild the DOM every time you add a new element to the page. To optimize this process, we can accumulate the whole variety of <span> inside a variable and then add them to the DOM all at once. If any element on the page mandates a set of operations, you should not constantly use a selector to access it. All you have to do is to save it to a variable.

Example:

$('body').html('

');

var startTime = (new Date()).getTime();
for (var i = 0; i< 1000; i++) {
    $('.someClass').append(''+i+'');
}
console.log('DOM append 1 by 1 time: ' +
   ((new Date()).getTime()- startTime));

$('.someClass').html('');

startTime = (new Date()).getTime();
var el = $('.someClass');
for (var i = 1000; i< 2000; i++) {
    el.append(''+i+'');
}
$('.someClass2').append(string);
console.log('DOM append 1 by 1 without reselecting the target element time: ' +
    ((new Date()).getTime()- startTime));

startTime = (new Date()).getTime();
var string = '';
for (var i = 1000; i< 2000; i++) {
    string += ''+i+'';
}

$('.someClass2').append(string);
console.log('DOM append all together time: ' +
    ((new Date()).getTime() - startTime));

Result of code execution:

DOM append 1 by 1 time: 958
DOM append 1 by 1 without reselecting a target element time: 163
DOM append altogether time: 60

Avoiding of repeated selections of the same element results in 16-fold gain in performance, while appending the entire set of elements at a time speeds up the code almost 6-fold.

Optimize Selectors

Using of JQuery selectors is a common practice in Web applications development. A standard project may contain hundreds of selectors. Also, you can optimize the elements selection. In that case, if we know in which blocks the needed elements are located , we have to refine our selection:

startTime = (new Date()).getTime();

for (var i = 0; i< 1000; i++) {
    $('.cl');
}    

console.log('Simple select of elements time: ' +
    ((new Date()).getTime() - startTime));

startTime = (new Date()).getTime();

for (var i = 0; i< 1000; i++) {
    $('someClass2 .cl_0');
}

console.log('Target select of elements time: ' +
    ((new Date()).getTime()- startTime));

Result of code execution:

Simple select of elements time: 553
Target select of elements time: 180

As demonstrated by the test, selector refinement boosts selection rate three-fold.

Delegate Events

Binding of events to an element results in DOM change. To reduce the number of changes, remember of event bubbling and delegate it. Say, for example, a page has a menu with buttons inside. In this case, you do not need to create a separate event for each button press. The following suffices:

  1. Create a single menu click event (it will bubble)
  2. On click, define a target object
  3. In if..else, define object handlers
  4. Suppress bubbling

Example:

$(‘#menu’).click(function(e) {
    e = e || window.event;
    var target = e.target || e.srcElement;

    if (target.nodeName === ‘menuItemName’ ) {
        alert(‘!!!’);
        e.preventDefault();
        e.stopPropagation();
    }
});

The Downside

Well, quick code execution, of course, that’s fine. But over-optimization (especially in the early development stages) is a straight way to barely maintainable or even unreadable code. Initially, you should try to write a nice and readable code. Only then you should analyze its critical paths and slow paths to optimize performance.

JS Profiling Tools

To analyze the code, you can use the following dedicated profiling tools. They will allow you to find performance weaknesses of your applications mandating optimization. Below is a list of the most popular profilers:

  • YUI Profiler is a JS profiling library
  • Firebug is a popular and powerful Firefox plug-in. In addition to profiling, it offers a lot more other features and tools to the developer.
  • WebKit Web Inspector is an analog of Firebug for Safari / Chrome
  • IE Developer Tools is a profiler for the Microsoft’s Internet Explorer

Helpful Links

Leave a Reply