Chapter 19

Performance

A 2010 study by Jakob Nielsen found that “[s]lowness (or speed) makes such an impact that it can become one of the brand values customers associate with a site”1. If a user is forced to wait more than a few seconds for a page, they no longer feel in control of the experience and are likely to investigate alternatives.

On a more positive note, Nielsen also reveals that a speed increase of as little as 0.1 seconds can produce a noticeable lift in conversion rates. This is confirmed by a Google study from 20092, in which a 0.2 second delay decreased the number of searches by up to 0.36%, a significant number of customers for mass-market web apps.

Some say that performance is a feature and that it should be given the same priority as feature development, but even that may be underestimating its importance. Adequate performance is an absolute necessity that directly affects customer adoption of your app.


What can we do to improve it?

Minimise payload size

There are a number of ways we can send fewer and smaller files from the server to the browser.

First, optimise all the image files. Images often make up the bulk of the page payload and offer a good opportunity for performance improvements. Choose the best image format: JPEG for photographic images with a high number of colours and PNG for everything else. The GIF format may produce a slightly smaller file size for very small one- or two-colour images, but the fact that browsers render PNG images faster3 than GIF may negate this small difference. When in doubt, choose PNG over GIF.

Optimise image compression as well. For JPEG files this means trading off quality against file size. As a rule of thumb, about 85% quality (15% compression) gives a good balance between file size and quality. For PNG files, reduce the colour depth to accommodate the maximum number of colours: 24-bit for 16 million colours; 16-bit for 65,000 colours; 12-bit for 4,000 colours; or 8-bit for 256 colours. The 8-bit PNG is a special case and becomes a palette image, which means that every unique colour in the image increases the file size slightly. Remove duplicate colours and merge similar colours to further optimise palette images.

Many image editors insert metadata into image files, details which won’t be useful in your app. Use an image compressor to remove the metadata and perform other useful optimisations. Try OptiPNG4 for PNGs and jpegtran5 for JPEGs. A Google search for online PNG optimiser will highlight a number of online apps that remove metadata and perform other safe PNG compression techniques.

Next, strip out dead code. It’s inevitable that some HTML, CSS and JavaScript will become outdated during the development of a complex web app. If you can identify extraneous <div>s, JavaScript functions and CSS selectors, you can remove them from the page payload. This is usually a manual exercise for those with expert knowledge of the codebase: dynamic eval()s in JavaScript and configurable callbacks from Ajax make it almost impossible for an automated checker to be completely accurate about unnecessary code. You could still use tools like JSLint6 and Dust-Me Selectors7 to point you in the right direction.

Be sure to minify text files. JavaScript and CSS files often contain comments, formatting white space and lengthy descriptive variable names that are of no use to the browser or user. Run the production versions of these files through a code minifier to create smaller files (YUI Compressor8 is a widely used tool). HTML can also be compacted with tools like HtmlCompressor9, though usually to a lesser extent.

Text files can also be gzipped. Enable gzip compression on your web server for HTML, CSS and JavaScript files. To get the best compression out of the gzip algorithm, Google recommends10 that you code consistently: use lowercase whenever possible; use the same quote character for HTML attributes (single or double); and specify HTML attributes in the same order (always put the src attribute first in image markup, for example).

Larger files can be loaded on demand. If your app uses a large piece of JavaScript or CSS that is only applicable to a specific subsection or page, separate it out and load it only when those sections are used. Also consider lazy loading large images that are towards the bottom of longer pages and not initially visible. Lazy load plug-ins are available for most JavaScript frameworks, like the Yahoo! YUI ImageLoader11.

Implementing client-side form validation will lead to a few kilobytes extra JavaScript, but it will save users from the larger round trip to the server to download an error page.

Finally, use UTF-8 characters rather than entities. A UTF-8-encoded character will always occupy fewer bytes than an HTML entity equivalent. For example, the copyright symbol © is encoded in two bytes as a UTF-8 character, whereas the entities &copy; and &#169; are six bytes apiece, one per character.

Optimise caching

There’s no need to download app files twice: allow the browser to appropriately re-use files that have already been downloaded.

Place CSS and JavaScript in external files. Inline styles and scripts aren’t cached between pages whereas external files are.

Set your cache HTTP header fields. Configure your web server to send future Expires or Cache-Control: max-age HTTP header fields for images, JavaScript, CSS and other static files. Cache dates can be up to one year in the future. Get into the habit of including version numbers in JavaScript and CSS filenames, so that new releases break the long-term cache in browsers and force a new download.

img-19_1

Twitter sends Cache-Control and Expires header fields to cache its logo image for almost one year.

Make Ajax cacheable. Partial Ajax responses can also send a future Expires header field if the Ajax URL includes a relevant fingerprint, usually a timestamp. For example, if the Ajax code requests the latest five messages for a user, the URL should include the latest message timestamp: http://app.com/msg?t=1299318393. If the messages haven’t changed since the previous request, the timestamp and URL will be the same, and the browser will use the local cache.

Consider a shared content delivery network (CDN). Unless you’re a masochist, you’ll probably use one of the popular JavaScript frameworks to develop your app, as will most other web app authors. Rather than forcing the user to download the same JavaScript library for each web app, you can reference a shared version of the library that is cached between apps. Google offers most of the popular libraries on its CDN12. Twitter uses the Google version of jQuery, so if you reference the same file, Twitter users that visit your app won’t need to re-download jQuery.

Optimise traffic overhead

We can also speed things up by reducing the number and size of HTTP and DNS requests.

Reduce cookie size: the less information stored in a cookie, the less data is sent with each HTTP request from the browser to the server. Host static content on a cookieless domain: images, multimedia, JavaScript and CSS files should be served from a domain or subdomain on which cookies are not set or valid, to reduce the HTTP header size.

To minimise DNS lookups, files should be served from the same domain, with the exceptions of a shared CDN domain for libraries, a cookieless domain for static files, and perhaps specific domains for parallelised downloads (which we’ll cover shortly). You can further reduce traffic overhead by enabling Keep-Alives on your web server to support persistent HTTP connections.

Combine files: where possible, combine scripts or style sheets into a single file to remove HTTP overhead. Similarly, combine similar small images into a single image file and use the CSS sprite technique13 to display them individually.

Optimise code

By optimising our code, we can generate server-side output faster.

Start with your database queries and structure. Use the correct data types (date fields for dates, and so on) and create relevant indexes, particularly covered indexes. A covered index spans all of the fields necessary to satisfy all the criteria in a SELECT statement.

Your queries should only return the data you need: use the LIMIT clause and avoid SELECT* statements. Built-in database profiling tools, such as explain plans and slow query logs, will identify and help you to resolve query bottlenecks. Also consider denormalising14 data where appropriate, such as often-viewed reports that aggregate large amounts of old data that is unlikely to change.

Tune the database settings, which vary between database vendors. At the minimum, investigate the optimum memory buffer size and query cache size for your app and hardware combination. These settings specify how much data is stored and queried in memory rather than on disk, and how much memory is put aside to store the results of common queries.

Store session data and frequently used app cache data in a fast in-memory datastore like Redis15 or Memcached16.

Optimise rendering

There are many ways to help the browser fetch, store and display content more efficiently.

Minimise the size of the DOM. A large number of HTML elements means a slower download, slower calculation of CSS selectors and slower JavaScript DOM manipulation. Hundreds of DOM elements is typical; thousands of DOM elements may need optimisation. Use the JavaScript console in your web browser to check the number of DOM nodes with document.getElementsByTagName('*').length.

img-19_2

Using the JavaScript console in Google Chrome to check the number of DOM elements in Tumblr’s dashboard page.

Use the DOM efficiently. Reduce JavaScript DOM access by storing a cached reference to frequently used DOM elements. Minimise reflows and repaints by batching DOM changes: create node trees (document fragments) outside the flow of the main DOM and insert the finished structure into the DOM in a single update. Prefer interactions that don’t cause a reflow: a background fade rather than a change in size, for instance.

Flush the output of dynamic HTML pages after the <head> section. Most popular programming languages, including PHP and Ruby, provide a function for flushing the output buffer to the browser. If the <head> section is sent in an initial chunk, the browser can parse the code and start to download style sheets while the server generates the remainder of the page.

Include style sheets in the <head> and JavaScript files just before the </body> to optimise rendering.

Parallelise downloads. If your web app serves a large number of static files on a single page (map image tiles, perhaps), consider serving the files from multiple host names to work around the limit of approximately six simultaneous browser downloads per host. Each additional host will produce some DNS lookup overhead, so only consider this option if your app downloads more than ten static files on a page, and limit the maximum number of hosts to four. Spread files evenly across the hosts and ensure that any particular file is always served from the same host to enable caching.

img-19_3

A Firebug17 report reveals that Google Maps parallelises downloads across multiple domains.

Pre-fetch future components. Use idle time to fetch and cache images, style sheets or scripts that you expect the user will need later. This works best when the app has a strict workflow. For example, a search interface is always followed by a search results page. Interface components required for the search results page could be preloaded when the search page onload event fires: that is, when the initial search page components are fully downloaded.

Specify the character encoding in the HTTP headers or as the first line inside the <head> of HTML files to prevent the browser re-parsing the file.

Optimise CSS selectors. Browsers match CSS selectors from right to left, traversing up the DOM to check the validity of child selectors. To improve CSS performance, use explicit classes or IDs rather than generic element selectors and remove unnecessary traversal checks up the DOM18. For example, the selector div .container #form-error is evaluated from right to left, first by finding elements with the #form-error ID, then checking if they have an ancestor with a .container class, and then finally checking for an ancestor <div> element. This would be faster as a single #form-error selector, which still matches the same element (IDs should only be applied to a single element on a page) but without the additional checks. As an extra bonus, you’re reducing the size of the CSS file download by including fewer characters in the CSS selectors.

Finally, specify image dimensions in the HTML to avoid unnecessary reflows and repaints when the images load.

Summary

Performance speed is a crucial element of a user’s experience of an app, particularly because of the uptake of web-enabled mobile devices that have bandwidth, memory and processor constraints.

Web App Success book coverAlso available to buy in a beautiful limited edition paperback and eBook.

This work is licensed under a Creative Commons Attribution 4.0 International License.