Css Performance - Part 2

Introduction

In the first part I created a simple speed test harness and found the best way of creating 100 rows in a large table, regardless of the CSS conditions. Now I'm going to do the same for sorting.

This isn't a look at sort algorithms, because that has nothing to do with the CSS on the page - I am going to assume that a view model is being used and the view has already been sorted - so, what is the best way to reflect that in the DOM? Because therefore the speed I am measuring is how to alter the DOM and how long it takes the browser to render that change, most of my functions are just going to reverse a list - a simple and repeatable worst case in terms of sorting.

using appendChild

So, first off is perhaps the easiest approach. Because when we appendChild a node that is already present, it has the effect of removing it, so we loop through the rows backwards and add it on to the end of the array.

var reverseRows = function () {
	var tbl = document.getElementById("mytb").childNodes[0],
		tbody = tbl.childNodes[0], rows = [], i;

	for(i = tbody.childNodes.length-1; i >= 0; i--) {
		rows[rows.length] = tbody.childNodes[i];
	}

	for(i = 0; i < rows.length; i++) {
		tbody.appendChild(rows[i]);
	}
};

Variations: disconnected @tbody

One variation on this is to disconnect the tbody from the table before doing the sort and then re-attaching. This is what the Google Closure Library does (last time I checked).

var reverseRowsDc = function () {
	var tbl = document.getElementById("mytb").childNodes[0],
	tbody = tbl.childNodes[0];

	tbl.removeChild(tbody);

	// do reversal

	tbl.appendChild(tbody);
};

Variations: disconnected @table

And to be fair to the disconnected approach, I repeat this but disconnecting the table from the parent element.

var reverseRowsDc2 = function () {
	var tbl = document.getElementById("mytb").childNodes[0],
		tbody = tbl.childNodes[0], parent = tbl.parentNode;

	parent.removeChild(tbl);

	// do reversal

	parent.appendChild(tbl);
};

removeChild appendChild

In case there is a performance hit in doing appendChild with a connected element, here we remove all the rows before adding them again.

var reverseRowsRemoveAppend = function () {
	var tbl = document.getElementById("mytb").childNodes[0],
		tbody = tbl.childNodes[0], rows = [], i, row;

	for(i = tbody.childNodes.length-1; i >= 0; i--) {
		row = tbody.childNodes[i];
		rows[rows.length] = row;
		tbody.removeChild(row);
	}

	for(i = 0; i < rows.length; i++) {
		tbody.appendChild(rows[i]);
	}
};

add documentFragment

Another way of sorting is to add the rows in order to a document fragment and then adding that document fragment to the tbody.

var reverseRowsFrag = function() {
	var tbl = document.getElementById("testcontainer").childNodes[0],
		tbody = tbl.childNodes[0], i, rows=[],
		frag = document.createDocumentFragment();

	for(i = tbody.childNodes.length-1; i >= 0; i--) {
		rows.push(tbody.childNodes[i]);
	}

	for(i = 0; i < rows.length; i++) {
		frag.appendChild(rows[i]);
	}

	tbody.appendChild(frag);
};

innerHTML

innerHTML won the last speed attempt, so assuming that you either do not have event handlers or you are going re-attach the event handlers, here is an apprach to see if re-rendering the table from source html is quicker.

var reverseRowsIHTML = function() {
	var tbl = document.getElementById("testcontainer").childNodes[0],
		tbody = tbl.childNodes[0], rows = [], i,
		newContent = "", parent;

	for(i = tbody.childNodes.length-1; i >= 0; i--) {
		newContent += tbody.childNodes[i].outerHTML;
	}

	// This really is best case scenario, otherwise we might need to create on
	// a div and move it and then we are stuck with the previous situation
	tbl.parentNode.innerHTML = '<table><tbody>'+newContent+'</tbody></table>';
};

insertBefore

All of the attempts so far have assumed that we have sorted the rows and we need to re-order every row, even if it is already in the right position. In order to determine if it might grant a performance increase to only move those rows that have changed position, I am applying a test to reverse a row by using insertBefore - our worst case scenario if attempting to be clever about which rows are moved.

var reverseRowsInsertBefore = function () {
	var tbl = document.getElementById("mytb").childNodes[0],
		tbody = tbl.childNodes[0],
		rows = [], i, lastDomElement;

	for(i = tbody.childNodes.length-1; i >= 0; i--) {
		rows[rows.length] = tbody.childNodes[i];
	}

	lastDomElement = tbody.childNodes[0];
	for(i = 0; i < rows.length; i++) {
		tbody.insertBefore(rows[i], lastDomElement);
		lastDomElement = rows[i];
	}
};

insertBefore Half

And for comparison, using the insertBefore but only doing half as many. Every row has changed its row index, but insertBefore has only been called half as many times. A viable situation, particularly if the rows are already grouped.

var reverseRowsInsertBeforeDc = function () {
	var tbl = document.getElementById("mytb").childNodes[0],
		tbody = tbl.childNodes[0],
		rows = [], i, lastDomElement;

	tbl.removeChild(tbody);

	for(i = tbody.childNodes.length-1; i >= 0; i--) {
		rows[rows.length] = tbody.childNodes[i];
	}

	lastDomElement = tbody.childNodes[0];
	for(i = 0; i < rows.length; i++) {
		tbody.insertBefore(rows[i], lastDomElement);
		lastDomElement = rows[i];
	}

	tbl.appendChild(tbody);
};

The whole test page is available here.

Conclusion

First off, here is a table of the results I found.

Css Rules HTC Desire Chrome IE9 PP7 Opera Firefox 3.6 IE8 IE8 In IE7 Mode
appendChild 25 5095 321 340 98 56 508 315
appendChild 625 14260 197 274 63 76 354 202
appendChild d/c @tbody 25 5140 275 340 90 61 512 325
appendChild d/c @tbody 625 13490 290 538 57 267 370 217
appendChild d/c @table 25 6640 529 938 201 388 1075 926
appendChild d/c @table 625 16060 1697 1150 582 1341 1184 955
removeChild appendChild 25 5190 253 329 89 30 491 309
removeChild appendChild 625 14020 150 265 63 311 433 184
add documentFragment 25 6630 521 1143 206 1577 1192 892
add documentFragment 625 16010 1665 1348 594 2636 1268 979
all innerHTML 25 3570 1133 982 262 86 1401 1020
all innerHTML 625 12040 1466 1212 709 77 1499 1099
insertBefore 25 5103 231 301 78 42 464 269
insertBefore 625 10940 145 280 34 60 352 166
insertBefore half 25 5130 216 288 75 23 451 262
insertBefore half 625 10580 163 295 35 32 416 174
insertBefore d/c @tbody 25 5480 209 288 67 25 455 256
insertBefore d/c @tbody 625 15300 165 525 39 36 354 188
insertBefore half d/c@tbody 25 5050 203 278 63 27 441 244
insertBefore half d/c@tbody 625 15590 160 270 42 37 606 184

You'll notice that there is less agreement on the best approach. I decided to highlight the best results again, but disregard the results when only doing half as many operations so that I am focusing on the worst case. To help me analyse the results further I have again created another table showing the difference between the result and the best result horizontally in that category. Because the HTC Desire took longer and I didn't want it to sway the results I have disregarded the values for the sake of the total.

Css Rules HTC Desire Chrome IE9 PP7 Opera Firefox 3.6 IE8 IE8 In IE7 Mode Total
appendChild 25 1525 112 52 31 31 53 59 338
appendChild 625 3320 52 9 29 40 2 36 168
appendChild d/c @tbody 25 5140 275 340 90 61 512 325 1603
appendChild d/c @tbody 625 2550 145 273 23 231 18 51 741
appendChild d/c @table 25 6640 529 938 201 388 1075 926 4057
appendChild d/c @table 625 5120 1552 885 548 1305 832 789 5911
removeChild appendChild 25 5190 253 329 89 30 491 309 1501
removeChild appendChild 625 3080 5 0 29 275 81 18 408
add documentFragment 25 6630 521 1143 206 1577 1192 892 5531
add documentFragment 625 5070 1520 1083 560 2600 916 813 7492
all innerHTML 25 3570 1133 982 262 86 1401 1020 4884
all innerHTML 625 1100 1321 947 675 41 1147 933 5064
insertBefore 25 5103 231 301 78 42 464 269 1385
insertBefore 625 0 0 15 0 24 0 0 39
insertBefore half 25 5130 216 288 75 23 451 262 1315
insertBefore half 625 -360 18 30 1 -4 64 8 117
insertBefore d/c @tbody 25 5480 209 288 67 25 455 256 1300
insertBefore d/c @tbody 625 4360 20 260 5 0 2 22 309
insertBefore half d/c@tbody 25 5050 203 278 63 27 441 244 1256
insertBefore half d/c@tbody 625 4650 15 5 8 1 254 18 301

So, you can see that the best results are either insertBefore or appendChild. The surprising result is that the best choice changes with the amount of css on the page - something that will come in useful in later investigations. Again I only received a minor performance improvements disconnecting the tbody from the DOM and they were only in certain browsers like Firefox.

One interesting side note is the HTC Desire - it ran much faster re-generating the innerHTML - something to think about if you are reading this and implementing mobile side row sorting.

A lot more time could be spent talking about speed of table sorting, but hopefully I've covered enough to assume that a connected appendChild or a lazy insertBefore type method would be best.

MORE BY LUKE

Seven Surprising JavaScript 'Features'

Aurelia, less2css and bundling

blog comments powered by Disqus