Css Performance - Part 1

Introduction

Over the last month or so, I've been interested in website performance. There are a lot of useful tools that exist to help you get the most out the HTML/JavaScript/CSS Technologies - One resource in particular I've used is Google Page Speed. Among the recommendation is one to optimise the use of css selectors, however when I looked for evidence to support this rule, every link eventually went back to a Mozilla page written to help people write UI elements in Firefox using the XUL extensions.

What about IE? What about Web Kit? How much difference does CSS really make?

I've divided the task into a few parts.

  • Create a test harness that can measure performance speed.
  • Work out some common operations and the best way of performing these operations under different CSS conditions (and therefore establish what techniques are best used to determine fastest CSS practice).
  • Create some tests that measure the performance hit of some of the inefficient CSS described in the Mozilla article.

In this first blog article I will concentrate on the creating of a test harness.

A Test Harness

The first thing I want is to measure not just the time to run the JavaScript but the time it takes for the browser to render - to achieve this I'm using a setTimeout(...,0) in order to measure the time it would take between making a DOM change and the next time that some JavaScript can run. This isn't totally accurate, but it has seemed good enough to get some indications - I'm not concerned with an exact time for each operation, just an idea of the scale of time that different techniques and CSS make to the render and run time.

So, first I need an HTML page with a percentage bar, a start button and a results table.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=8" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Dom Changing Test</title>
<style type="text/css">
.tb { border-collapse: collapse; table-layout: fixed; width: 600px; height: 570px; padding: 0px; border: 0px; margin: 0px;}
.tb td { padding: 0px; border: 0px; margin: 0px; overflow: hidden;}
.percbar { width: 400px; height: 1.1em; border: black solid 1px;}
#percentage { width: 0px; height: 1.1em; color: white; background-color: green;}
</style>
</head>
<body>
 <a onClick="document.getElementById('starttest').style.display = 'none';StartTest();return false;" id="starttest" href="#">Start Tests</a>
 <div>
 <div id="percentage"></div>
 </div>

 <table>
 <thead>
 <tr><th>No.</th><th>Description</th><th>Selector Count</th><th>Selector</th>
 <th>Setup Time</th><th>Test Time</th><th>Render Time</th><th>Render+Test Time</th></tr>
 </thead>
 <tbody id="testresults">
 </tbody>
 </table>

 <div id="testcontainer">&nbsp;
 </div>
</body>
</html>

Next I need a test function that will run a setup funcion, the actual test, clean up and give me a measurement of the time into my results table.

var runTest = function(setupFunc, testFunc, testDescription, returnFunc) {

	setTimeout(function() {
		var setupStart = new Date().getTime();
		setupFunc();
		setTimeout(function() {

			var testStart = new Date().getTime();
			testFunc();
			var testEnd = new Date().getTime();
			var afterRender = function() {
				var renderEnd = new Date().getTime();

				cleanUp();

				var tr = document.createElement("tr"), td;

				for(var i = 0; i < testDescription.length; i++) {
					td = document.createElement("td");
					td.innerHTML = testDescription[i];
					tr.appendChild(td);
				}

				td = document.createElement("td");
				td.innerHTML = ""+(testStart-setupStart);
				tr.appendChild(td);

				td = document.createElement("td");
				td.innerHTML = ""+(testEnd-testStart);
				tr.appendChild(td);

				td = document.createElement("td");
				td.innerHTML = ""+(renderEnd-testEnd);
				tr.appendChild(td);

				td = document.createElement("td");
				td.innerHTML = ""+(renderEnd-testStart);
				tr.appendChild(td);

				document.getElementById("testresults").appendChild(tr);

				setTimeout(returnFunc, 500);
			};
			setTimeout(afterRender, 0);
		}, 0);
	}, 0);
};

I return with an additional 500ms to give the browser some time to sort anything out that it needs to. My next problem is that the runTest is asynchronous - if I want to use it to run multiple tests then I could end up with some horrible test definitions. It would be nice if the tests could be defined as an array and the asynchronous nature wrapped up. To do this, I make use of a capture variable and some recursion. The function can also keep track of how many tests have been performed and update the percentage bar.

var runTestArray = function(testsToRun, totalCount) {
	if  (!totalCount) {
		totalCount = testsToRun.length;
	}

	var percentage = (totalCount - testsToRun.length) / totalCount;
	document.getElementById("percentage").style.width = parseInt(percentage*400)+"px";

	if  (testsToRun.length === 0) {
		document.getElementById("percentage").innerHTML = "Finished!";
		return;
	}

	runTest(testsToRun[0].setup,
			testsToRun[0].test,
			testsToRun[0].description,
			function() {
				runTestArray(testsToRun.slice(1), totalCount);
			});
};

A first test

So, now we have a (simple) test harness, I shall create an initial speed test. A common operation on large JavaScript applications is to make changes to a table. Specifically we will write a test for adding new rows to an existing table. Perhaps one of the biggest debates for JavaScript DOM performance in this area is the argument between unofficial but widely supported innerHTML property on DOM nodes and using DOM methods such as createElement to create the elements individually.

Those who have tried to use innerHTML with tables in IE have probably discovered that IE does not support innerHTML for elements in the structure of a table. So TD.innerHTML works and DIV.innerHTML = ">table... works but TABLE.innerHTML, TBODY.innerHTML, TR.innerHTML all fail in IE. They seem to strip out the table elements and add the contents of the TD to whatever table element you are altering, regardless of what is allowed, ending up with SPAN elements being the direct children of TABLE elements.

We will bypass this by sticking new table rows inside a dummy<table><tbody></tbody</table> and then extracting out the rows and moving them to our existing table - hence creating rows using innerHTML and measuring the performance with a real life hindrance.

Because the point of this is to measure the performance with relation to CSS, we will perform each test with both 25 and 625 css selectors that target things on the table. We will go into more detail on using CSS selectors in a later part, but at this point I just want to determine if a particular approach is better with lots of CSS versus not very much. I'm also going to go into some detail over the order in which elements are created and added to the DOM - do we do createElement, add it to the DOM and then set the class name or is it better to set the className and then add it to the DOM?

Mix of innerHTML and createElement

First off is an attempt that most people might go for - they know innerHTML is faster but don't want a more complex solution for the sake of speed.

var create100Rows = function () {
	var tbl = document.createElement("table"),
		tbody = document.createElement("tbody"),
		i, j, tr, td, testcontainer = document.getElementById("testcontainer"), gs,
		onClick = function() {alert("a");};

	tbl.className = "tb";
	tbl.id = "mytb";
	testcontainer.appendChild(tbl);
	tbl.appendChild(tbody);

	for(i = 0; i < 100; i++) {
		tr = document.createElement("tr");
		tbody.appendChild(tr);
		for(j = 0; j < 100; j++) {
			td = document.createElement("td");
			tr.appendChild(td);

			td.innerHTML = "<span class="unm">_</span>";
			gs = td.childNodes[0];
			td.className = usedClasses[Math.floor(Math.random()*usedClasses.length)];
			if  (j === 3 || j === 8 || j === 10) {
				addEvent(td, "Click", onClick);
			}
		}
	}
};

all createElement

Next we have a solution that does not use innerHTML at all.

var create100RowsNoIH = function() {
	var tbl = document.createElement("table"),
		tbody = document.createElement("tbody"),
		i, j, tr, td, testcontainer = document.getElementById("testcontainer"), span,
		onClick = function() {alert("a");};

	tbl.className = "tb";
	tbl.id = "mytb";
	testcontainer.appendChild(tbl);
	tbl.appendChild(tbody);

	for(i = 0; i < 100; i++) {
		tr = document.createElement("tr");
		tbody.appendChild(tr);
		for(j = 0; j < 100; j++) {
			td = document.createElement("td");
			tr.appendChild(td);
			span = document.createElement("span");
			td.appendChild(span);
			span.innerHTML = "_";
			span.className = "unm";

			td.className = usedClasses[Math.floor(Math.random()*usedClasses.length)];
			if  (j === 3 || j === 8 || j === 10) {
				addEvent(td, "Click", onClick);
			}
		}
	}
};

All innerHTML

And as discussed earlier in the blog our solution that just uses innerHTML to create our elements.

var create100RowsUIH = function () {
	var tbl = document.createElement("table"),
		tbody = document.createElement("tbody"),
		i, j, tr, td, testcontainer = document.getElementById("testcontainer"),
		dcTb = document.createElement("div"), tb,
		onClick = function() {alert("a");};

	tbl.className = "tb";
	tbl.id = "mytb";
	testcontainer.appendChild(tbl);
	tbl.appendChild(tbody);

	for(i = 0; i < 100; i++) {
		tr = "<table><tbody><tr>";
		for(j = 0; j < 100; j++) {
			td = "<td class=""+usedClasses[Math.floor(Math.random()*usedClasses.length)]+""><span class="unm">_</span></td>";
			tr = tr+td;
		}
		tr = tr+"</tr></tbody></table>";
		dcTb.innerHTML = tr;
		tr = dcTb.childNodes[0].childNodes[0].childNodes[0];

		for(j = 0; j < 100; j++) {
			td = tr.childNodes[j];
			if  (j === 3 || j === 8 || j === 10) {
				addEvent(td, "Click", onClick);
			}
		}
		tbody.appendChild(tr);
	}
};

Mix of both - but disconnected from the DOM

Next in our first variation we take the first example but don't attach it to the DOM until after we have set properties like the className and innerHTML.

var create100RowsDc = function () {
	var tbl = document.createElement("table"),
		tbody = document.createElement("tbody"),
		i, j, tr, td, testcontainer = document.getElementById("testcontainer"),
		onClick = function() {alert("a");};

	tbl.className = "tb";
	tbl.id = "mytb";
	testcontainer.appendChild(tbl);

	for(i = 0; i < 100; i++) {
		tr = document.createElement("tr");
		for(j = 0; j < 100; j++) {
			td = document.createElement("td");
			td.innerHTML = "<span class="unm">_</span>";
			td.className = usedClasses[Math.floor(Math.random()*usedClasses.length)];
			if  (j === 3 || j === 8 || j === 10) {
				addEvent(td, "Click", onClick);
			}
			tr.appendChild(td);
		}
		tbody.appendChild(tr);
	}
	tbl.appendChild(tbody);
};

all document.createElement but disconnected from the DOM

And for comparison, our function that uses createElement, but we wait until everything is ready before attaching to the DOM.

var create100RowsNoIHDc = function() {
	var tbl = document.createElement("table"),
		tbody = document.createElement("tbody"),
		i, j, tr, td, testcontainer = document.getElementById("testcontainer"), span,
		onClick = function() {alert("a");};

	tbl.className = "tb";
	tbl.id = "mytb";
	testcontainer.appendChild(tbl);
	tbl.appendChild(tbody);

	for(i = 0; i < 100; i++) {
		tr = document.createElement("tr");
		for(j = 0; j < 100; j++) {
			td = document.createElement("td");
			span = document.createElement("span");
			span.innerHTML = "_";
			span.className = "unm";

			td.className = usedClasses[Math.floor(Math.random()*usedClasses.length)];
			if  (j === 3 || j === 8 || j === 10) {
				addEvent(td, "Click", onClick);
			}
			td.appendChild(span);
			tr.appendChild(td);
		}
		tbody.appendChild(tr);
	}
};

Results

You can view the row constructing test here.

I tested some browsers on my Windows 7 machine and a HTC Desire (Android) and here are the results.

Css Rules HTC Desire Chrome IE9 PP7 Opera Firefox 3.6 IE8 IE8 In IE7 Mode
Mix of innerHTML and createElement 25 9073 778 9302 31212 4760 10773 10809
Mix of innerHTML and createElement 625 13159 1845 10197 34063 7381 10741 10727
all createElement 25 3019 684 9850 2458 4738 10392 10725
all createElement 625 14160 1857 11356 3704 9305 11440 11461
All innerHTML 25 3098 728 976 343 1094 1198 1092
All innerHTML 625 11393 1777 1110 709 2083 1241 1167
Mix of both - but d/c from the DOM 25 1894 687 2072 378 837 2161 2357
Mix of both - but d/c from the DOM 625 11742 1808 2250 757 1821 2206 2417
all document.createElement but d/c 25 1671 622 2252 396 1314 2285 2437
all document.createElement but d/c 625 12386 1823 2400 830 2301 2397 2507

Conclusion

I'll let you draw your own conclusions as to the gap between IE8 on a dual core windows 7 machine and WebKit on a mobile phone. I've highlighted the results that give the best performance, both with lots of css and minimal css. I've created a table (below) which is the time with the optimal time subtracted - E.g. the loss in performance from optimal if you implement a particular technique.

Css Rules HTC Desire Chrome IE9 PP7 Opera Firefox 3.6 IE8 IE8 In IE7 Mode Total
Mix of innerHTML and createElement 25 7402 156 8326 30869 3923 9575 9717 69968
Mix of innerHTML and createElement 625 1766 68 9087 33354 5560 9500 9560 68895
all createElement 25 1348 62 8874 2115 3901 9194 9633 35127
all createElement 625 2767 80 10246 2995 7484 10199 10294 44065
All innerHTML 25 1427 106 0 0 257 0 0 1790
All innerHTML 625 0 0 0 0 262 0 0 262
Mix of both - but d/c from the DOM 25 223 65 1096 35 0 963 1265 3647
Mix of both - but d/c from the DOM 625 349 31 1140 48 0 965 1250 3783
all document.createElement but d/c 25 0 0 1276 53 477 1087 1345 4238
all document.createElement but d/c 625 993 46 1290 121 480 1156 1340 5426

This makes it obvious that even though it's not always the best, using innerHTML (even though we had get references afterwards and hack it in by creating a fake holding table) is faster overall.

Next Part - Sorting!

MORE BY LUKE

Seven Surprising JavaScript 'Features'

Aurelia, less2css and bundling

blog comments powered by Disqus