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.
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).
Variations: disconnected @table
And to be fair to the disconnected approach, I repeat this but disconnecting the table from the parent element.
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.
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.
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.
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.
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.
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.