I am a Javascript newbie. After trying out many Javascript and Jquery plugins to sort my HTML table and ending up disappointed, I decided to implement my own Javascript code for sorting HTML tables. The code I wrote is an update from W3Schools.
function sortFunctionNumeric(n) {
var table, rows, switching, i, x, y, shouldSwitch, dir, switchcount = 0;
table = document.getElementById("reportingTable");
switching = true;
//Set the sorting direction to ascending:
dir = "asc";
/*Make a loop that will continue until
no switching has been done:*/
while (switching) {
//start by saying: no switching is done:
switching = false;
rows = table.rows;
/*Loop through all table rows (except the
first, which contains table headers):*/
for (i = 1; i < (rows.length - 1); i++) {
//start by saying there should be no switching:
shouldSwitch = false;
/*Get the two elements you want to pare,
one from current row and one from the next:*/
x = rows[i].getElementsByTagName("TD")[n];
y = rows[i + 1].getElementsByTagName("TD")[n];
/*check if the two rows should switch place,
based on the direction, asc or desc:*/
if (dir == "asc") {
if (Number(x.innerHTML) > Number(y.innerHTML)) {
//if so, mark as a switch and break the loop:
shouldSwitch = true;
break;
}
} else if (dir == "desc") {
if (Number(x.innerHTML) < Number(y.innerHTML)) {
//if so, mark as a switch and break the loop:
shouldSwitch = true;
break;
}
}
}
if (shouldSwitch) {
/*If a switch has been marked, make the switch
and mark that a switch has been done:*/
rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
switching = true;
//Each time a switch is done, increase this count by 1:
switchcount++;
} else {
/*If no switching has been done AND the direction is "asc",
set the direction to "desc" and run the while loop again.*/
if (switchcount == 0 && dir == "asc") {
dir = "desc";
switching = true;
}
}
}
}
Now the sorting works perfectly fine. However, it is very slow!
I deal with a lot of rows of daqta(depending on project, it can go up to 9000 rows). Is there a way to speed up my Javascript code?
I am a Javascript newbie. After trying out many Javascript and Jquery plugins to sort my HTML table and ending up disappointed, I decided to implement my own Javascript code for sorting HTML tables. The code I wrote is an update from W3Schools.
function sortFunctionNumeric(n) {
var table, rows, switching, i, x, y, shouldSwitch, dir, switchcount = 0;
table = document.getElementById("reportingTable");
switching = true;
//Set the sorting direction to ascending:
dir = "asc";
/*Make a loop that will continue until
no switching has been done:*/
while (switching) {
//start by saying: no switching is done:
switching = false;
rows = table.rows;
/*Loop through all table rows (except the
first, which contains table headers):*/
for (i = 1; i < (rows.length - 1); i++) {
//start by saying there should be no switching:
shouldSwitch = false;
/*Get the two elements you want to pare,
one from current row and one from the next:*/
x = rows[i].getElementsByTagName("TD")[n];
y = rows[i + 1].getElementsByTagName("TD")[n];
/*check if the two rows should switch place,
based on the direction, asc or desc:*/
if (dir == "asc") {
if (Number(x.innerHTML) > Number(y.innerHTML)) {
//if so, mark as a switch and break the loop:
shouldSwitch = true;
break;
}
} else if (dir == "desc") {
if (Number(x.innerHTML) < Number(y.innerHTML)) {
//if so, mark as a switch and break the loop:
shouldSwitch = true;
break;
}
}
}
if (shouldSwitch) {
/*If a switch has been marked, make the switch
and mark that a switch has been done:*/
rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
switching = true;
//Each time a switch is done, increase this count by 1:
switchcount++;
} else {
/*If no switching has been done AND the direction is "asc",
set the direction to "desc" and run the while loop again.*/
if (switchcount == 0 && dir == "asc") {
dir = "desc";
switching = true;
}
}
}
}
Now the sorting works perfectly fine. However, it is very slow!
I deal with a lot of rows of daqta(depending on project, it can go up to 9000 rows). Is there a way to speed up my Javascript code?
Share Improve this question edited Dec 11, 2019 at 10:01 cloned 6,7424 gold badges30 silver badges44 bronze badges asked Dec 11, 2019 at 9:44 Lenin MishraLenin Mishra 1812 silver badges16 bronze badges 9-
3
Remove the rows from the DOM, sort them, re-add them to the DOM ->
document.createDocumentFragement()
– Andreas Commented Dec 11, 2019 at 9:48 - Actually just hide the rows gives a very god effekt. Render is usually the heavy thing in this. – Griffin Commented Dec 11, 2019 at 9:56
-
2
It's slow because you're using a bad sorting algorithm (after a quick-glance it looks like bubble-sort with polynomial time
O(n^2)
because it iterates through the table for each row (thefor
inside thewhile
). Use JavaScript's built-in sorting algorithm atArray.prototype.sort
instead. – Dai Commented Dec 11, 2019 at 9:58 -
How is your
sortFunctionNumeric
meant to be invoked? Isn
meant to be the column index? (I note that your function will fail if there is acolspan
orrowspan
in the table). – Dai Commented Dec 11, 2019 at 10:00 -
@Dai Yes. The
n
is the column index. – Lenin Mishra Commented Dec 11, 2019 at 10:02
3 Answers
Reset to default 12It helps to avoid implementing sorting algorithms in browser JavaScript because JavaScript's built-in Array.prototype.sort
method will be much faster even if you end-up implementing the same sort algorithm (IIRC most JS engines will probably use QuickSort anyway).
Here's how I'd do it:
- Get all of the
<tr>
elements in a JavaScriptArray
.- You need to use
querySelectorAll
in conjunction withArray.from
becausequerySelectorAll
does not return an array, it actually returns aNodeListOf<T>
- but you can pass this intoArray.from
to convert it to anArray
.
- You need to use
- Once you have the
Array
, you can useArray.prototype.sort(parison)
with a custom callback to extract the data from the<td>
child of the two<tr>
elements being pared, and then pare the data (using thex - y
trick when paring numeric values. Forstring
values you'll want to useString.prototype.localeCompare
, e.g.return x.localeCompare( y )
. - After the
Array
is sorted (which should not take more than a few milliseconds for even a table with tens of thousands of rows, as QuickSort is really quick!) re-add each<tr>
usingappendChild
of the parent<tbody>
.
My implementation in TypeScript is below, along with a working sample with valid JavaScript in the script-runner located underneath:
// This code has TypeScript type annotations, but can be used directly as pure JavaScript by just removing the type annotations first.
function sortTableRowsByColumn( table: HTMLTableElement, columnIndex: number, ascending: boolean ): void {
const rows = Array.from( table.querySelectorAll( ':scope > tbody > tr' ) );
rows.sort( ( x: HTMLtableRowElement, y: HTMLtableRowElement ) => {
const xValue: string = x.cells[columnIndex].textContent;
const yValue: string = y.cells[columnIndex].textContent;
// Assuming values are numeric (use parseInt or parseFloat):
const xNum = parseFloat( xValue );
const yNum = parseFloat( yValue );
return ascending ? ( xNum - yNum ) : ( yNum - xNum ); // <-- Neat parison trick.
} );
// There is no need to remove the rows prior to adding them in-order because `.appendChild` will relocate existing nodes.
for( let row of rows ) {
table.tBodies[0].appendChild( row );
}
}
function onColumnHeaderClicked( ev: Event ): void {
const th = ev.currentTarget as HTMLTableCellElement;
const table = th.closest( 'table' );
const thIndex: number = Array.from( th.parentElement.children ).indexOf( th );
const ascending = ( th.dataset as any ).sort != 'asc';
sortTableRowsByColumn( table, thIndex, ascending );
const allTh = table.querySelectorAll( ':scope > thead > tr > th' );
for( let th2 of allTh ) {
delete th2.dataset['sort'];
}
th.dataset['sort'] = ascending ? 'asc' : 'desc';
}
My sortTableRowsByColumn
function assumes the following:
- Your
<table>
element uses<thead>
and has a single<tbody>
- You're using a modern browser that supports
=>
,Array.from
,for( x of y )
,:scope
,.closest()
, and.remove()
(i.e. not Internet Explorer 11). - Your data exists as the
#text
(.textContent
) of the<td>
elements. - There are no
colspan
orrowspan
cells in the table.
Here's a runnable sample. Simply click the column headers to sort in ascending or descending order:
function sortTableRowsByColumn( table, columnIndex, ascending ) {
const rows = Array.from( table.querySelectorAll( ':scope > tbody > tr' ) );
rows.sort( ( x, y ) => {
const xValue = x.cells[columnIndex].textContent;
const yValue = y.cells[columnIndex].textContent;
const xNum = parseFloat( xValue );
const yNum = parseFloat( yValue );
return ascending ? ( xNum - yNum ) : ( yNum - xNum );
} );
for( let row of rows ) {
table.tBodies[0].appendChild( row );
}
}
function onColumnHeaderClicked( ev ) {
const th = ev.currentTarget;
const table = th.closest( 'table' );
const thIndex = Array.from( th.parentElement.children ).indexOf( th );
const ascending = !( 'sort' in th.dataset ) || th.dataset.sort != 'asc';
const start = performance.now();
sortTableRowsByColumn( table, thIndex, ascending );
const end = performance.now();
console.log( "Sorted table rows in %d ms.", end - start );
const allTh = table.querySelectorAll( ':scope > thead > tr > th' );
for( let th2 of allTh ) {
delete th2.dataset['sort'];
}
th.dataset['sort'] = ascending ? 'asc' : 'desc';
}
window.addEventListener( 'DOMContentLoaded', function() {
const table = document.querySelector( 'table' );
const tb = table.tBodies[0];
const start = performance.now();
for( let i = 0; i < 9000; i++ ) {
let row = table.insertRow( -1 );
row.insertCell( -1 ).textContent = Math.ceil( Math.random() * 1000 );
row.insertCell( -1 ).textContent = Math.ceil( Math.random() * 1000 );
row.insertCell( -1 ).textContent = Math.ceil( Math.random() * 1000 );
}
const end = performance.now();
console.log( "IT'S OVER 9000 ROWS added in %d ms.", end - start );
} );
html { font-family: sans-serif; }
table {
border-collapse: collapse;
border: 1px solid #ccc;
}
table > thead > tr > th {
cursor: pointer;
}
table > thead > tr > th[data-sort=asc] {
background-color: blue;
color: white;
}
table > thead > tr > th[data-sort=desc] {
background-color: red;
color: white;
}
table th,
table td {
border: 1px solid #bbb;
padding: 0.25em 0.5em;
}
<table>
<thead>
<tr>
<th onclick="onColumnHeaderClicked(event)">Foo</th>
<th onclick="onColumnHeaderClicked(event)">Bar</th>
<th onclick="onColumnHeaderClicked(event)">Baz</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>9</td>
<td>a</td>
</tr>
<!-- 9,000 additional rows will be added by the DOMContentLoaded event-handler when this snippet is executed. -->
</tbody>
</table>
A word on performance:
According to Chrome 78's Developer Tools' Performance analyzer, on my puter, the performance.now()
calls indicate the rows were sorted in about 300ms, however the "Recalculate style" and "Layout" operations which happens after the JavaScript has stopped running took 240ms and 450ms respectively (690ms total relayout time, plus the 300ms sort time means it took a full second (1,000ms) from click-to-sorted).
When I changed the script such that the <tr>
elements are added to an intermediate DocumentFragment
instead of the <tbody>
(so that each .appendChild
call is guaranteed to not cause a reflow/layout, instead of just assuming that .appendChild
won't trigger a reflow) and re-ran the performance test my result timing figures were more-or-less identical (they were actually slightly higher by about 120ms total after 5 repeats, for an average time of (1,120ms) - but I'll put that down to the browser JIT playing-up.
Here's the changed code inside sortTableRowsByColumn
:
function sortTableRowsByColumn( table, columnIndex, ascending ) {
const rows = Array.from( table.querySelectorAll( ':scope > tbody > tr' ) );
rows.sort( ( x, y ) => {
const xValue = x.cells[columnIndex].textContent;
const yValue = y.cells[columnIndex].textContent;
const xNum = parseFloat( xValue );
const yNum = parseFloat( yValue );
return ascending ? ( xNum - yNum ) : ( yNum - xNum );
} );
const fragment = new DocumentFragment();
for( let row of rows ) {
fragment.appendChild( row );
}
table.tBodies[0].appendChild( fragment );
}
I reckon the performance is relatively slow because of the Automatic Table Layout algorithm. I'll bet if I change my CSS to use table-layout: fixed;
the layout times will shrink. (Update: I tested it with table-layout: fixed;
and surprisingly that didn't improve performance at all - I can't seem to get better times than 1,000ms - oh well).
<!DOCTYPE html>
<html>
<head>
<script>
function sort_table(tbody, index, sort = (a, b) => {
if(a < b) return -1; if(a > b) return 1; return 0;})
{
var list = []
for (var i = 0; i < tbody.children.length; i++)
list.push([tbody.children[i].children[index].innerText, tbody.children[i]]);
list.sort((a, b) => sort(a[0], b[0]));
var newtbody = document.createElement('tbody');
for (var i = 0; i < list.length; i++)
newtbody.appendChild(list[i][1]);
tbody.parentNode.replaceChild(newtbody, tbody);
return newtbody;
}
</script>
</head>
<body>
<h2>Unsorted</h2>
<table>
<thead>
<tr>
<th>Name</th>
<th>Last Name</th>
<th>Nationality</th>
<th>Born</th>
</tr>
</thead>
<tbody>
<tr><td>Henry</td><td>Cavill</td>
<td>British</td><td>5 May 1983</td></tr>
<tr><td>Gal</td><td>Gadot</td>
<td>Israeli</td><td>30 April 1985</td></tr>
<tr><td>Olga</td><td>Kurylenko</td>
<td>Ukrainian</td><td>14 November 1979</td></tr>
<tr><td>Vincent</td><td>Cassel</td>
<td>French</td><td>23 November 1966</td></tr>
</tbody>
</table>
<script>
var table = document.getElementsByTagName('table')[0];
var named = table.cloneNode(true);
var dated = table.cloneNode(true);
document.body.innerHTML += "<h2>Sorted by name</h2>";
document.body.appendChild(named);
sort_table(named.children[1], 0); //by name
document.body.innerHTML += "<h2>Sorted by date</h2>";
document.body.appendChild(dated);
sort_table(dated.children[1], 3, (a, b) => { //by date
if (new Date(a) < new Date(b)) return -1;
if (new Date(a) > new Date(b)) return 1;
return 0;
});
</script>
</body>
</html>
9000 rows (numbers) in 156 ms - 190 ms
I needed to sort a table on a column of addresses: 3 Autumn Lane, 1 Brandon Way, 10 Nature Lane, 100 Pristine Place ... And wanted the sort on street names then house number.
To get that to work, I modified the sortTableRowsByContent function to split numbers from text. Also, since some of my columns have blank values, I hide them when they sort:
function sortTableRowsByColumn( table, columnIndex, ascending ) {
const rows = Array.from( table.querySelectorAll( ':scope > tbody > tr' ) );
rows.sort( ( a, b ) => {
const aValue = a.cells[columnIndex].textContent;
const bValue = b.cells[columnIndex].textContent;
const aNum = parseFloat( aValue );
const bNum = parseFloat( bValue );
const aNew = aValue.replace(/^\d{1,3} /, "");
const bNew = bValue.replace(/^\d{1,3} /, "");
if (ascending){
return aNew.localeCompare(bNew) || parseInt(aNum) - parseInt(bNum);
} else {
return bNew.localeCompare(aNew) || parseInt(bNum) - parseInt(aNum);
}
} );
for ( i=0; i < rows.length; i++) {
if (rows[i].getElementsByTagName("td")[columnIndex].textContent == '') {
// this hides the blank rows
rows[i].style.display = "none";
} else {
// this restores blanks if a different column is sorted
rows[i].style.display = "";
}
};
for( let row of rows ) {
table.tBodies[0].appendChild( row );
}
}