Article Metadata
Created: isalento (25 Nov 2009)
Last edited: wmseto (10 Aug 2012)
This article gives valuable guidance for building faster web applications. The article is a collection of guidelines from various sources.
Tips & tricks in this article were collected from the developer experiences when creating an example application.
First understand the big picture and the major component of the stack affecting the performance. It is of utmost importance to understand what can and cannot be optimized in JavaScript without touching the browser codebase. A good starting point for this study is to first take a look at the JavaScript Performance Stack (depicted in the figure).
Pick your battles. It is generally a good approach to first optimize those parts which give you the biggest improvements.
There are many interrelated components that play an instrumental role in the real-life web application performance such as those responsible for layout, rendering, parsing HTML, marshaling, DOM, CSS formatting, JavaScript -- as you see, JavaScript is only one part of the performance equation.
The most expensive operations tend to be reflowing the layout and repainting. Although you as a JavaScript developer cannot optimize browser layout or painting algorithms you can still implicitly affect the performance of these expensive operations by trying to avoid triggering there expensive operations unnecessarily. A real-life example of IE8 tells us that layout and rendering tasks takes most time on IE8 (see webcast at -20:00 mins)
Below are some examples of common reasons for slow JavaScript performance that you as a JavaScript developer can easily fix and make your web application to perform better instantly:
DOM access
Interaction with the DOM is usually slower than normal JavaScript code. Interaction with the DOM is usually inevitable, but try to minimize it. For instance, dynamically creating HTML with strings and setting the innerHTML is usually faster than creating HTML with DOM methods.
eval
Whenever possible, avoid the eval method because significant overhead is involved in script evaluation.
with
Using with statements creates additional scope objects that slow variable access and create ambiguities.
for-in loops
Traverse arrays use the traditional {{{1}}}instead of for-in loops. Unfortunately, most JavaScript environments have a slow implementation of for-in loops.
Slow:
function addMethod(object, property, code) {
object[property] = new Function(code);
}
addMethod(myObj, 'methodName', 'this.localVar=foo');
Faster:
function addMethod(object, property, func) {
object[property] = func;
}
addMethod(myObj, 'methodName', function () { 'this.localVar=foo'; });
Although seen as a convenience, with construct introduces an extra scope to search each time variable is referenced and the contents of that scope is not known at compile time.
Slow:
with (test.object) {
foo = 'Value of foo property of object';
bar = 'Value of bar property of object';
}
Faster:
var myObj = test.object;
myObj.foo = 'Value of foo property of object';
myObj.bar = 'Value of bar property of object';
Slow:
var object = ['foo', 'bar'], i;
for (i = 0; i < object.length; i++) {
try {
// do something that throws an exception
} catch (e) {
// handle exception
}
}
Faster:
var object = ['foo', 'bar'], i;
try {
for (i = 0; i < object.length; i++) {
// do something
}
} catch (e) {
// handle exception
}
Slow:
var i,
str = '';
function globalScope() {
for (i=0; i < 100; i++) {
str += i; // here we reference i and str in global scope which is slow
}
}
globalScope();
Faster:
function localScope() {
var i,
str = '';
for (i=0; i < 100; i++) {
str += i; // i and str in local scope which is faster
}
}
localScope();
Slow:
var sum = 0;
for (var i in arr) {
sum += arr[i];
}
Faster:
var sum = 0;
for (var i = 0, len = arr.length; i < len; i++) {
sum += arr[i];
}
Slow:
Faster:
Slow:
var min = Math.min(a, b);
arr.push(val);
Faster:
var min = a < b ? a : b;
arr[arr.length] = val;
Slow:
setInterval('doSomethingPeriodically()', 1000);
setTimeOut('doSomethingAfterFiveSeconds()', 5000);
Faster:
setInterval(doSomethingPeriodically, 1000);
setTimeOut(doSomethingAfterFiveSeconds, 5000);
var car = new Object();
car.color = "red";
car.type = "sedan"
var car = {
color : "red";
type : "sedan"
}
var url = window.location.href;
Keep script comments to a minimun or avoid them altogether, especially inside functions, loops and arrays. Comments unnecessarily slow down script execution and increase file size. For example,
function someFunction()
{
var person_full_name="somename"; /* stores the full name*/
}
function someFunction()
{
var name="somename";
}
Slow:
function doSomething(text) {
var divs = document.getElementsByTagName('div'),
text = ['foo', /* ... n ... */, 'bar'];
for (var i = 0, l = divs.length; i < l; i++) {
divs[i].innerHTML = text[i];
}
}
Faster:
function doSomethingFaster(text) {
var doc = document,
divs = doc.getElementsByTagName('div'),
text = ['foo', /* ... n ... */, 'bar'];
for (var i = 0, l = divs.length; i < l; i++) {
divs[i].innerHTML = text[i];
}
}
If you need to access an element (e.g. the head) inside a big loop using a localized DOM access ( get in the example) is faster.
Faster:
function doSomethingElseFaster() {
var get = document.getElementsByTagName;
for (var i = 0, i < 100000; i++) {
get('head');
}
}
Wrong Way:
var d=35;
for (var i=0; i<1000; i++) {
y += Math.sin(d)*10;
}
Better Approach:
var d = 55;
var math_sind = Math.sin(d)*10;
for (var i=0; i<1000; i++) {
y += math_sind;
}
Bad Approach:
The length of the array arr is recalculated every time the loop iterates.
for (var i = 0; i < arr.length; i++) {
// do something
}
Better Approach:
Better way is to to cache the length of the array:
for (var i = 0, len = arr.length; i < len; i++) {
// do something
}
Sources: Variable Performance, slides High Performance Kick Ass Web Apps Video at JSCONF 2009
Slow:
Faster:
// JavaScript
function loadScript(src, callback) {
var head = document.getElementsByTagName('head')[0],
script = document.createElement('script');
done = false;
script.setAttribute('src', src);
script.setAttribute('type', 'text/javascript');
script.setAttribute('charset', 'utf-8');
script.onload = script.onreadstatechange = function() {
if (!done && (!this.readyState || this.readyState == 'loaded' || this.readyState == 'complete')) {
done = true;
script.onload = script.onreadystatechange = null;
if (callback) {
callback();
}
}
}
head.insertBefore(script, head.firstChild);
}
// load the my-script-file.js and display an alert dialog once the script has been loaded
loadScript('my-script-file.js', function() { alert('my-script-file.js loaded.'); });
Sources:
Using Apache the Expires HTTP header and the max-age directive of the Cache-Control HTTP header in server responses can be configures in .htaccess. The syntax is as follows:
ExpiresDefault "<base> [plus] {<num> <type>}*" ExpiresByType type/encoding "<base> [plus] {<num> <type>}*"
Example: ExpiresActive On ExpiresByType image/png "access plus 1 year"
Source: Apache mod_expires
Below is a simple configuration to gzip not only JavaScript and CSS but also HTML, XML and JSON. To accomplish this, the following must be set in the Apache .htaccess:
AddOutputFilterByType DEFLATE text/html text/css text/plain text/xml application/x-javascript application/json
Source:
Sources
Even Faster Websites - Steve Souders at SXSW ‘09
Even Faster Websites- Steve Souders at SXSW ‘09
Source: Efficient JavaScript - DOM
Slow DOM performance can be traced back into the following three main causes:
Extensive DOM manipulation | Extensive use of DOM API is a well-known cause of slowness. |
Script triggers too many reflows or repaints | As a consequence of DOM manipulation, reflowing the layout and repainting are very expensive. |
Slow approach to locating nodes in the DOM | Locating a desired node(s) in the DOM is potential bottleneck if the DOM is sizable and/or complex. |
Repaint | Repainting happens when something is made visible or hidden without altering the layout of the document. For example if an outline is added to an element, its background color is changed of its visibility is changed. Repainting is an expensive operation (paint events demo). |
Reflow | Reflow happens whenever the DOM is manipulated in a way it affects the layout. For example, style is changed to affect the layout, className property is changed or browser window size is changed. Once an element needs to be reflown, its children will also be reflown and any elements appearing after the element in the DOM. Finally, everything is repainted. Reflows are even more expensive operations, than repainting. In many cases reflowing is comparable to layout out the entire page again (reflow demo videos). |
Slow:
var list = ['foo', 'bar', 'baz'],
elem,
contents;
for (var i = 0; i < list.length; i++) {
elem = document.createElement('div');
content = document.createTextNode(list[i]);
elem.appendChild(content);
document.body.appendChild(elem);
}
Faster:
var fragment = document.createDocumentFragment(),
list = ['foo', 'bar', 'baz'],
elem,
contents;
for (var i = 0; i < list.length; i++) {
elem = document.createElement('div');
content = document.createTextNode(list[i]);
fragment.appendChild(content);
}
document.body.appendChild(fragment);
Faster:
var orig = document.getElementById('container'),
clone = orig.cloneNode(true),
list = ['foo', 'bar', 'baz'],
elem,
contents;
clone.setAttribute('width', '50%');
for (var i = 0; i < list.length; i++) {
elem = document.createElement('div');
content = document.createTextNode(list[i]);
elem.appendChild(content);
clone.appendChild(elem);
}
original.parentNode.replaceChild(clone, original);
<nowiki>
var model = { title: 'My Test Page'; },
template = [];
template.push('<h1>' + model.title + '<h1>');
template.push('<div>Another Test Element<div>');
document.getElementById(containerId).innerHTML = template.join('');
// alternatively you can use concat() -- see string concatenation test results
</nowiki>
Slow:
var subElem = document.createElement('div'),
elem = document.getElementById('animated');
elem.appendChild(subElem);
elem.style.width = '320px';
Faster:
var subElem = document.createElement('div'),
elem = document.getElementById('animated');
elem.style.display = 'none';
elem.appendChild(subElem);
elem.style.width = '320px';
elem.style.display = 'block';
Slow:
var elem = document.getElementById('animated');
elem.style.fontSize = (elem.offsetWidth / 10) + 'px';
elem.firstChild.style.marginleft = (elem.offsetWidth / 20) + 'px';
Faster:
var elem = document.getElementById('animated'),
elemWidth = elem.offsetWidth;
elem.style.fontSize = (elemWidth / 10) + 'px';
elem.firstChild.style.marginleft = (elemWidth / 20) + 'px';
Slow:
var elem = document.getElementById('styled');
elem.style.background = 'blue';
elem.style.color = 'white';
Faster:
<code html4strict>
<style type="text/css">
div { background: white; color: black; }
div.active { background: blue; color: white; }
</style>
...
var elem = document.getElementById('styled').className = 'active';
Faster:
var elem = document.getElementById('styled');
elemStyle = 'background: blue; color: white;';
elem.setAttribute('style', elemStyle);
Changing the class name of an element is a nice way to use JavaScript to dynamically change elements. Performance varies from browser to browser, but generally it is faster to change an element's visual appearance directly via the Javascript style attribute, rather than to change a class name on that element.
Slow: div.active { border: 1px solid red; }
Faster (for a one element):
var container = document.getElementById('container');
container.style.border = '1px solid red';
The above method appears to be more efficient when changing a specific number of items. Sometimes a single class name change is effective however. If you need to change all elements under a given container for example, it is more efficient to change the class name of a parent container which holds the affected elements and let CSS do what it does best.
Faster (if multiple child elements of a container need to be changed): // by changing the class name of the container, all of its child div elements will be updated #container.active div { border: 1px solid red; }
Depending on the specific case at hand you should use the method which gives you the best performance (without sacrificing too much of the separation of concerns benefits of externally defined CSS).
Source:
Slow:
var elements = document.getElementsByTagName('*'); // searches every element, slow
for (i = 0; i < elements.length; i++) {
if (element[i].hasAttribute('selected')) { // continues even through element was found
...
}
}
Faster:
var elements = document.getElementById('parent').childNodes; // we know the element is a child of parent
for (i = 0; i < elements.length; i++) {
if (element[i].nodeType == 1 && element[i].hasAttribute('selected') { // first test for valid node type
...
break; // break out of the loop if we found what we were looking for
}
}
Slow:
var elems = document.getElementsByTagName('div');
for (var i = 0; i < elems.length; i++) {
elems[i].appendChild(document.createTextNode(i));
}
Faster:
var elems = document.getElementsByTagName('div'),
temp = [];
for (var i = 0; i < elems.length; i++) {
temp[i] = elems[i]; // first a build static list of elements to modify
}
for (var i = 0; i < temp.length; i++) {
temp[i].appendChild(document.createTextNode(i)); // perform modifications on static list instead of live NodeList
}
temp = null;
Slow:
document.getElementById('elem').propertyOne = 'value of first property';
document.getElementById('elem').propertyTwo = 'value of second property';
document.getElementById('elem').propertyThree = 'value of third property';
Faster:
var elem = document.getElementById('elem').propertyOne = 'value of first property';
elem.propertyTwo = 'value of second property';
elem.propertyThree = 'value of third property'
Slow:
var frame = parent.frames['frameId'].document,
container = frame.getElementById('contentId'),
content = frame.createElement('div');
content.appendChild(frame.createTextNode('Some content'));
container.appendChild(content);
Faster:
var frame = parent.frames['frameId'].document,
container = frame.getElementById('contentId'),
content = frame.createElement('div');
content.appendChild(frame.createTextNode('Some content'));
container.appendChild(content);
// nullify references to frame
frame = null;
container = null;
content = null;
var xhr = new XMLHttpRequest ();
xhr.open('GET', url, false);
xhr.onreadystatechange = function () {
if (this.readyState == 4) {
clearTimeout(timeout);
// do something with response data
}
}
var timeout = setTimeout(
function () {
xhr.abort();
// call error callback
},
60*1000 // timeout after a minute
);
xhr.send();
JSON: {{{1}}}
Equivalent using custom data format and String and RegExp methods:
that.contacts = o.responseText.split("\\c");
for (var n = 0, len = that.contacts.length, contactSplit; n < len; n++) {
contactSplit = that.contacts[n].split("\\a");
that.contacts[n] = {};
that.contacts[n].n = contactSplit[0];
that.contacts[n].e = contactSplit[1];
that.contacts[n].u = contactSplit[2];
that.contacts[n].r = contactSplit[3];
that.contacts[n].s = contactSplit[4];
that.contacts[n].f = contactSplit[5];
that.contacts[n].a = contactSplit[6];
that.contacts[n].d = contactSplit[7];
that.contacts[n].y = contactSplit[8];
}
Sources:
Using native scrolling via scrollTo() performs significantly better as it does not trigger reflow.
Position value | Description |
---|---|
static | Default. An element with position: static always has the position the normal flow of the page gives it (a static element ignores any top, bottom, left, or right declarations) |
relative | An element with position: relative moves an element relative to its normal position, so left:20 adds 20 pixels to the element's left position |
absolute | An element with position: absolute is positioned at the specified coordinates relative to its containing block. The element's position is specified with the left, top, right, and bottom properties. |
fixed | An element with position: fixed is positioned at the specified coordinates relative to the browser window. The element's position is specified with the left, top, right, and bottom properties. The element remains at that position regardless of scrolling. |
Source:
Slow:
setInterval(function() {
animateFirst(arg);
}, 50);
setInterval(function() {
animateSecond(arg);
}, 50);
function animateFirst(arg) {};
function animateSecond(arg) {};
Faster:
setInterval(function() {
animateFirst(arg);
animateSecond(arg);
}, 50);
function animateFirst(arg) {};
function animateSecond(arg) {};
Sources, further reading:
To trade smoothness for speed means that while you may want to move an animation 1 pixel at a time, the animation and subsequent reflows may in that case use 100% of the CPU and the animation will seem jumpy as the browser is forced to drop frames to update the flow. Moving the animated element by e.g. 5 pixels at a time may seem slightly less smooth on faster machines, but won’t cause CPU thrashing that easily on mobile devices.
Slow:
var elems = [first, ..., last]; // an array which holds say 1000 references to element to which assign the event handlers to
for (var i, l = elems.length; i++; i < l) {
elems[i].onclick = function() {};
elems[i].onblur = function() {};
}
Faster:
//HTML
<button id="doSomething">Click me to do something</button> // you can add more of elements without the need to worry about binding event handlers
// JS
document.addEventListener('click', function(event) { eventController(event); }, false);
document.addEventListener('blue', function(event) { eventController(event); }, false);
function eventController(event) {
// inspect the event object internals and do something wise
if (event.target.id === 'doSomething') {
doSomething();
}
}
function doSomething() {}
Slow:
window.onresize = resizeHandler; // fires excessively during resize
Faster:
function SomeObject() {
var self = this;
this.lastExecThrottle = 500; // limit to one call every "n" msec
this.lastExec = new Date();
this.timer = null;
this.resizeHandler = function() {
var d = new Date();
if (d-self.lastExec < self.lastExecThrottle) {
// This function has been called "too soon," before the allowed "rate" of twice per second
// Set (or reset) timer so the throttled handler execution happens "n" msec from now instead
if (self.timer) {
window.clearTimeout(self.timer);
}
self.timer = window.setTimeout(self.resizeHandler, self.lastExecThrottle);
return false; // exit
}
self.lastExec = d; // update "last exec" time
// At this point, actual handler code can be called (update positions, resize elements etc.)
// self.callResizeHandlerFunctions();
}
}
var someObject = new SomeObject();
window.onresize = someObject.resizeHandler;
Source:
In a typical scenario this will not turn the loading indicator visible:
showLoadingIndicator();
doSomethingExpensive();
Workaround is to use setTimeout as follows (please take extra care when using this anti-pattern as what it does is actually delays the execution in order to display the loading indicator in the UI):
function switchViews() {
setTimeout(function() {
showLoadingIndicator();
}, 0);
setTimeout(function() {
doSomethingExpensive();
}, 50);
}
Source:
To limit the scope of the reflow to as few nodes as possible you should avoid changing a class on wrapper (or body) element(s) which affects the display of many child nodes. Additionally, that may result in re-resolving style on the entire document and for a large DOM that could lock up the browser for a while.
Source:
Avoid tables for layout. As if you needed another reason to avoid them, tables often require multiple passes before the layout is completely established because they are one of the rare cases where elements can affect the display of other elements that came before them on the DOM. Imagine a cell at the end of the table with very wide content that causes the column to be completely resized. This is why tables are not rendered progressively in all browsers and yet another reason why they are a bad idea for layout.
It is recommended to use a fixed layout ( table-layout: fixed) for data tables to allow a more efficient layout algorithm. This will allow the table to render row by row according to the CSS 2.1 specification.
Source:
Source:
In WRT 1.x you can remove default blue borders in tab navigation mode with the following CSS. a:hover, a:focus { outline: none; }
In tabbed navigation mode the focus may lost while switching views. A fix to this issue is to place a read-only <input> text element (can be absolutely positioned, zero-sized and transparent) under the element to be focused after switching the view and focus():ing on it after the view changes.
/* CSS */
#top {
position: absolute;
top: 0px;
width: 0px;
height: 0px;
border: 0px;
background: transparent;
}
<!-- HTML -->
<input id="top" type="text" readonly="readonly"/>
// JavaScript
widget.setNavigationEnabled(false); // tabbed navigation mode
function toggleViews() {
hideFirstView();
showSecondView();
document.getElementById('top').focus();
}
You may need to alter the positioning of the element by modifying the top property to position it underneath the first focusable element depending on your layout.
There is no exhaustive fix to this bug but it seems to help if you set the styles via onblur and onfocus event handlers instead of using CSS pseudo selector :hover.
So instead of :hover in CSS:
You utilize onfocus and onblur event handlers and set the styles using JavaScript:
document.getElementById('focusable').onfocus = function () {
this.style.backgroundColor = 'blue';
}
document.getElementById('focusable').onblur = function () {
this.style.backgroundColor = 'inherit';
}
S60 3.x and 5.0 devices do not animated gif images if they have been set as display: none at any point during their lifespan regardless of subsequent display value. A workaround to this bug is to avoid display: none and use position: absolute and alter left property to move the animated gif outside of the visible viewport to hide it ( left: -100%) and bring it back ( left: 0).
Alternatively you can fold the animated gif into another element whose style you set in a similar fashion.
<!-- animation hidden -->
<img style="position: absolute; left: -100%;" src="animated.gif" />
<!-- animation visible -->
<img style="position: absolute; left: 0;" src="animated.gif" />
for (var i in document.images) {
document.images[i].src = "#";
}
Interaction such as scrolling is extremely slow if background-repeat property is set to repeat-x or repeat-y combined with a background-image of a size which needs to be repeated multiple times to fill in the element's background. A workaround is to avoid using repeating backgrounds i.e. use a big enough background-image with background-repeat: no-repeat. For example, a commonly used CSS design pattern -- which degrades the performance and should be avoided -- is to create a continuous vertical gradient background using a background image of width 1px which is repeated horizontally across the whole element width.
This optimization pattern is a tradeoff between the initial load time and the subsequent interaction smoothness. In a typical scenario using a bigger background-image (longed load time) with background-repeat: no-repeat delivers a far superior overall UX over using a smaller repeating background.
Slow:
Faster:
NB! These optimization patterns are aimed at jQuery 1.2.6. Recent versions and the Sizzle selector engine alleviate some of these bottlenecks.
Sources Advanced jQuery with John Resig JavaScript Function Call Profiling
remove(), hmtl() and empty() have order of n2 time complexity, that is T(n) = O(n2). Minimizing the use of these methods is recommended in jQuery versions prior to 1.3.3. .
append, prepend, before, after methods are expensive as well as taking a chunk of HTML and serializing it ($("Hello World!
")).The process behind these manipulations methods is the following: cleaning the input string, converting the string into a DOM fragment and injecting it into the DOM.
Pay attention to the selectors you use. E.g. if you want to hide paragraphs which are direct children of div elements there are multiple ways to do it.
Using the selector below will try to find all div elements, loops thought all of them and find all p relative to the div, merge and figure out unique results. Making sure that the returned set is unique is where the most time is spent, and is very slow especially with a large DOM tree.
Slow:
A one way to optimize the selector is to match direct children of div (this may require you to re-factor the DOM structure of your app and may not be feasible in every scenario). This alternative flatter selector will try to find all div elements, loops thought all child elements and verifies if element is p.
Faster:
Depending on the size of the array, looping through it using {{{1}}} instead of jQuery.each(myArray) may be faster.
Toggling element visibility via .css({'display':'none'}) and .css({'display':'block'}); is faster than using convenience functions .show(), .hide() and toggle(), especially while working with large number of elements. Depending on the rendering engine used, .css() is also faster than using .addClass() and .removeClass().
Slow:
$('#elementToHide').hide();
$('#elementToShow').show();
Faster:
$('#elementToHide').css({'display':'none'});
$('#elementToShow').css({'display':'block'});
For dealing with extremely large set of DOM elements, you may want to consider disabling stylesheets as follows:
// show all elements with a class of “special_hide”
$('#special_hide').attr('disabled', 'true');
// hide all elements with a class of “special_hide”
$('#special_hide').attr('disabled', 'false');
Sources
This article is originally authored by Anssi Kostiainen and licensed under a Creative Commons Attribution, Non Commercial - Share Alike 2.5 license.
---
Copyright notice: Nokia may include links to sites on the Internet that are owned or operated by third parties. If you choose to use such links to such sites you agree to review and accept such site's rules of use before using such site. You access the third-party sites at your own risk. Nokia does not assume any responsibility for material created or published by such third-party sites. By providing a link to a third party site Nokia does not imply that Nokia endorses the site or the products or services referenced in such third party site.