2

I am trying to update the body of html table using javascript.

There are two methods to do that

html table:

    <table>
     <thead>
      <tr>
        <th>Title</th>
      </tr>
     </thead>
     <tbody>
     </tbody>
    </table>

Method1(string interpolation):

    document.querySelector('table tbody').innerHTML= 
    '<tr><td>some text</td></tr><tr><td>some text</td></tr>'

Method2:

    const table = document.querySelector("table");
    const row = table.insertRow(0);
    const cell1 = row.insertCell(0);
    cell1.innerHTML = 'some text';

Which method has a better performance and why?

Suppose that every 1 second we have to update the entire body of the table and we have 100 rows

Note: I only want to know about performance and ignore other concerns like security

Mohanad Walo
  • 384
  • 3
  • 6
  • 1
    [fastest-dom-insertion](https://stackoverflow.com/questions/634878/fastest-dom-insertion#634960), also, since no answers have suggested it, you may want to look into [DocumentFragment](https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment) – Ryan Wilson Dec 25 '22 at 01:07

4 Answers4

5

Avoid Changing the DOM as Much as Possible

One of the most resource sapping operations we as front end developers deal with are changes to the DOM. Reflows and repaints involve a ton of computations the browser must deal with so keep interactions and changes to the DOM to a minimum. We can add, remove, and modify HTML and text by using a documentFragment as a temporary document to build upon. a docFrag isn't attached to the DOM so any operations done on the docFrag does not affect the DOM.

In Test A and Test B all operations are done on a docFrag -- the DOM will have 2 repaints and 2 reflows per test. In order to fully appreciate how detrimental reflows and repaints are, go to this test suite. Therein are 4 test cases TEST A and TEST B are the same as the Stack Snippets provided in this answer -- TEST C is TEST A without a docFrag and TEST D is TEST B without a docFrag. As for how many reflow/repaints does TEST C/D trigger I didn't bother to count (we can safely assume far more than a paltry 2 TEST A/B does).

Note: all tests (Snippets and Benches) have the same data input consisting of a multidimensional array of 100 rows and 3 columns and each cell's content is a 3 digit number.

JSBench.Me - TEST A/B/C/D

And the winner is...

TEST A dominated

Test A

documentFragment and HTMLTableElement Methods

const data=[[591,917,494],[198,200,592],[319,593,343],[149,708,760],[289,132,762],[966,587,225],[921,510,888],[175,283,918],[944,852,330],[537,518,558],[896,927,461],[324,360,719],[800,421,524],[634,868,548],[182,340,239],[636,760,786],[860,744,616],[213,512,587],[274,236,190],[861,996,552],[761,649,814],[121,471,554],[385,538,813],[802,522,861],[468,479,870],[209,238,180],[210,314,782],[682,581,644],[996,375,580],[635,586,252],[538,640,141],[650,788,716],[654,666,578],[583,573,787],[948,968,708],[993,177,355],[404,187,596],[275,312,556],[820,481,133],[598,541,618],[424,574,753],[271,257,560],[294,246,553],[240,698,833],[860,597,219],[796,295,378],[497,834,902],[168,647,239],[745,988,788],[572,356,490],[274,957,519],[698,402,673],[798,522,743],[595,677,416],[369,786,154],[691,424,502],[465,820,533],[827,966,761],[297,947,385],[817,930,803],[609,567,369],[223,981,890],[275,387,404],[407,578,779],[713,595,428],[499,986,421],[241,310,591],[713,328,239],[152,949,826],[438,840,708],[478,114,571],[274,304,105],[239,253,916],[573,281,263],[179,502,936],[725,639,245],[467,542,488],[515,923,784],[464,258,573],[582,709,761],[138,734,836],[376,572,680],[361,478,709],[924,683,538],[379,677,378],[435,850,167],[950,546,976],[236,724,194],[314,525,639],[362,715,573],[320,965,799],[973,717,627],[122,856,371],[169,702,269],[580,826,127],[949,530,791],[625,845,701],[748,570,277],[669,955,453],[279,239,867]];

const T = document.querySelector("table");

const genData = (table, tArray) => {
  let R = tArray.length;
  let C = tArray[0].length;
  const tB = document.createElement("tbody");
  const frag = document.createDocumentFragment();
  
  for (let r=0; r < R; r++) {
    let row = tB.insertRow();
    for (let c=0; c < C; c++) {
      row.insertCell().textContent = tArray[r][c];
    }
  }
  table.tBodies[0].remove(); // 1 reflow 1 repaint
  frag.append(tB);
  table.append(frag); // 1 reflow 1 repaint
}

genData(T, data);
<table>
  <thead>
    <tr>
      <th>A</th>
      <th>B</th>
      <th>C</th>
    </tr>
  </thead>
  <tbody></tbody>
</table>

Test B

documentFragment and Rendering HTML

const data=[[591,917,494],[198,200,592],[319,593,343],[149,708,760],[289,132,762],[966,587,225],[921,510,888],[175,283,918],[944,852,330],[537,518,558],[896,927,461],[324,360,719],[800,421,524],[634,868,548],[182,340,239],[636,760,786],[860,744,616],[213,512,587],[274,236,190],[861,996,552],[761,649,814],[121,471,554],[385,538,813],[802,522,861],[468,479,870],[209,238,180],[210,314,782],[682,581,644],[996,375,580],[635,586,252],[538,640,141],[650,788,716],[654,666,578],[583,573,787],[948,968,708],[993,177,355],[404,187,596],[275,312,556],[820,481,133],[598,541,618],[424,574,753],[271,257,560],[294,246,553],[240,698,833],[860,597,219],[796,295,378],[497,834,902],[168,647,239],[745,988,788],[572,356,490],[274,957,519],[698,402,673],[798,522,743],[595,677,416],[369,786,154],[691,424,502],[465,820,533],[827,966,761],[297,947,385],[817,930,803],[609,567,369],[223,981,890],[275,387,404],[407,578,779],[713,595,428],[499,986,421],[241,310,591],[713,328,239],[152,949,826],[438,840,708],[478,114,571],[274,304,105],[239,253,916],[573,281,263],[179,502,936],[725,639,245],[467,542,488],[515,923,784],[464,258,573],[582,709,761],[138,734,836],[376,572,680],[361,478,709],[924,683,538],[379,677,378],[435,850,167],[950,546,976],[236,724,194],[314,525,639],[362,715,573],[320,965,799],[973,717,627],[122,856,371],[169,702,269],[580,826,127],[949,530,791],[625,845,701],[748,570,277],[669,955,453],[279,239,867]];

const T = document.querySelector("table");

const genData = (table, tArray) => {
  
  let R = tArray.length;
  let C = tArray[0].length;
  const tB = document.createElement("tbody");
  const frag = document.createDocumentFragment();
  
  for (let r=0; r < R; r++) {
    tB.innerHTML += `<tr></tr>`;
    for (let c=0; c < C; c++) {
      tB.rows[r].innerHTML += `<td>${tArray[r][c]}</td>`;
    }
  }
  table.tBodies[0].remove(); // 1 reflow 1 repaint
  frag.append(tB);
  table.append(frag); // 1 reflow 1 repaint
}

genData(T, data);
<table>
  <thead>
    <tr><th>A</th><th>B</th><th>C</th></tr>
  </thead>
  <tbody></tbody>
</table>
zer00ne
  • 41,936
  • 6
  • 41
  • 68
2

Let's test the performance with 1000 runs of table body updates. To compare apples to apples, both methods replace the whole table with 100 rows with each run:

function method1() {
  // add 100 rows:
  document.querySelector('#method1 tbody').innerHTML = '<tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr> <tr><td>some text</td></tr>';
}

function method2() {
  const tbody = document.querySelector('#method2 tbody');
  tbody.innerHTML = '';
  // add 100 rows:
  for(let i = 1; i <= 100; i++) {
    tbody.insertRow(0).insertCell(0).innerHTML = `row ${i} text`;
  }
}

let start = new Date();
for(let i = 0; i < 1000; i++) {
  method1();
}
let end = new Date();
console.log('method1:', end - start, 'ms');

start = new Date();
for(let i = 0; i < 1000; i++) {
  method2();
}
end = new Date();
console.log('method2:', end - start, 'ms');
<table id="method1">
 <thead>
  <tr>
    <th>Method 1</th>
  </tr>
 </thead>
 <tbody>
 </tbody>
</table>
<table id="method2">
 <thead>
  <tr>
    <th>Method 2</th>
  </tr>
 </thead>
 <tbody>
 </tbody>
</table>

Console log:

method1: 178 ms
method2: 463 ms

As suspected, method1 is 2.5x faster than method2. This makes sense, because to replace the tbody in method2 you need to empty it out first, and you have 200 method calls instead of a single update.

Peter Thoeny
  • 7,379
  • 1
  • 10
  • 20
  • @MisterJojo: Well, if you take the code as presented, method1 adds 2 rows, method2 would add 100,000 rows. – Peter Thoeny Dec 25 '22 at 02:22
  • if you add 100,000 with method 1, you will see that method 2 is much faster. the purpose of this test is to decide between the 2 methods and not to disadvantage one of them: see https://stackoverflow.com/questions/74910967/update-the-body-of-html-method-using-javascript/74911014#74911014 – Mister Jojo Dec 25 '22 at 02:27
  • I updated my answer: changed method1 and method2 from 2 rows to 100 rows. – Peter Thoeny Dec 25 '22 at 02:33
  • your 2 new versions are not comparable and do not allow to decide between them. – Mister Jojo Dec 25 '22 at 02:45
  • @MisterJojo: Please do not edit my answer, you introduced an error, method2 showed 100 * 1000 rows. Now fixed. I restored my answer. – Peter Thoeny Dec 25 '22 at 03:22
  • it does not change the fact that your way of testing is not fair. – Mister Jojo Dec 25 '22 at 03:24
1

JS code is faster !

test by yourself...
Using text means using an HTML interpreter to generate DOM elements, while JS code does this directly, and here are particularly optimized to deal with html tables.

const 
  tableBody1 = document.querySelector('#method1 tbody')
, tableBody2 = document.querySelector('#method2 tbody')
, loopMax      = 50
, replacements = 100
  ;
function method1()
  {
  tableBody1.innerHTML += '<tr><td>some text</td></tr><tr><td>some text</td></tr>';
  }
function method2()
  {
  tableBody2.insertRow().insertCell().textContent = 'some text'; 
  tableBody2.insertRow().insertCell().textContent = 'some text'; 
  }
console.time('method 1 - HTML') 
for (let rep = replacements; rep--;)
  {
  tableBody1.innerHTML = '';
  for (let i = 0; i < loopMax; i++) method1();  
  }
console.timeEnd('method 1 - HTML') 

console.time('method 2 - JS code') 
for (let rep = replacements; rep--;)
  {
  tableBody2.innerHTML = '';
  for (let i = 0; i < loopMax; i++) method2();
  }
console.timeEnd('method 2 - JS code')
<table id="method1">
 <thead> <tr> <th>Method 1</th> </tr> </thead>
 <tbody></tbody>
</table>
<table id="method2">
 <thead> <tr> <th>Method 2</th> </tr> </thead>
 <tbody></tbody>
</table>
Mister Jojo
  • 20,093
  • 6
  • 21
  • 40
  • This answer adds 1000 rows in method1, and 1000 in method2. The OP has 100 rows that needs to be replaced once a sec, so it does not answer the question. – Peter Thoeny Dec 25 '22 at 03:27
  • @PeterThoeny You are in bad faith, even with 100 elements the insertion of HTML text is 45 times slower. – Mister Jojo Dec 25 '22 at 03:33
  • Ok, so now you reduced that to adding 100 rows in both methods. It still does not reflect the OP's need to replace all 100 table rows every second. – Peter Thoeny Dec 25 '22 at 03:43
  • @PeterThoeny Ok, I added 100 overrides and the JS code is still the fastest. – Mister Jojo Dec 25 '22 at 03:51
1

It is interesting to see how heated a discussion can become around a relatively simple subject. The question was, which of two methods would provide a better performance when applied for generating a relatively small table (100 rows). The tests that were devised in other answers here (including my original version) were considered to be biased (or "unfair") by at least someone here.

However, it seems that one fact is accepted by everyone here: changes to the DOM should be made as infrequently as possible. Each call of .innerHTML comes at the cost of recalculating a lot of page elements and some heavy formatting work needs to be done by the browser.

In the following - largely rewritten tests - I compare the generation of a 100 row table between method 1 and method 2. In order to get some larger time values I repeat each test n times (here: n=200). It turns out that for the relatively small tables there is not that much of a difference (method 1 seems to be marginally faster). Method 2 will probably overtake method 1 for larger tables. Feel free to try that out.

function method1(arr) {
  document.querySelector('#method1 tbody').innerHTML= 
  arr.map(([a,b,c])=>`<tr><td>${a}</td><td>${b}</td><td>${c}</td></tr>`).join("\n");
}

function method2(arr) {
  tbody=document.querySelector('#method2 tbody');
  tbody.innerHTML="";
  arr.forEach(([a,b,c])=>{
   const row=tbody.insertRow();
   row.insertCell().textContent = a;
   row.insertCell().textContent = b;
   row.insertCell().textContent = c;
  })
}

function test(fn,name,dat){
  const n=200,start=new Date();
  for(let i = 0; i < n; i++) fn(dat);
  console.log(`${name}: ${new Date()-start}ms`);
};
const arr=[[591,917,494],[198,200,592],[319,593,343],[149,708,760],[289,132,762],[966,587,225],[921,510,888],[175,283,918],[944,852,330],[537,518,558],[896,927,461],[324,360,719],[800,421,524],[634,868,548],[182,340,239],[636,760,786],[860,744,616],[213,512,587],[274,236,190],[861,996,552],[761,649,814],[121,471,554],[385,538,813],[802,522,861],[468,479,870],[209,238,180],[210,314,782],[682,581,644],[996,375,580],[635,586,252],[538,640,141],[650,788,716],[654,666,578],[583,573,787],[948,968,708],[993,177,355],[404,187,596],[275,312,556],[820,481,133],[598,541,618],[424,574,753],[271,257,560],[294,246,553],[240,698,833],[860,597,219],[796,295,378],[497,834,902],[168,647,239],[745,988,788],[572,356,490],[274,957,519],[698,402,673],[798,522,743],[595,677,416],[369,786,154],[691,424,502],[465,820,533],[827,966,761],[297,947,385],[817,930,803],[609,567,369],[223,981,890],[275,387,404],[407,578,779],[713,595,428],[499,986,421],[241,310,591],[713,328,239],[152,949,826],[438,840,708],[478,114,571],[274,304,105],[239,253,916],[573,281,263],[179,502,936],[725,639,245],[467,542,488],[515,923,784],[464,258,573],[582,709,761],[138,734,836],[376,572,680],[361,478,709],[924,683,538],[379,677,378],[435,850,167],[950,546,976],[236,724,194],[314,525,639],[362,715,573],[320,965,799],[973,717,627],[122,856,371],[169,702,269],[580,826,127],[949,530,791],[625,845,701],[748,570,277],[669,955,453],[279,239,867]];

test(method1,"method 1",arr);
test(method2,"method 2",arr);
<table id="method1">
 <thead>
  <tr>
    <th>Method 1</th>
    <th>Column 2</th>
    <th>Column 3</th>
  </tr>
 </thead>
 <tbody>
 </tbody>
</table>
<table id="method2">    
 <thead>
  <tr>
    <th>Method 2</th>
    <th>Column 2</th>
    <th>Column 3</th>
  </tr>
 </thead>
 <tbody>
 </tbody>
</table>
Carsten Massmann
  • 26,510
  • 2
  • 22
  • 43
  • adding `tbody.innerHTML = '';` is not fair. PO is askiing for hundred row to add. You will see in my answer that JS code is faster. – Mister Jojo Dec 25 '22 at 01:40
  • When comparing the performance on generating small tables with around 100 rows there is hardly a difference between the two methods. – Carsten Massmann Dec 25 '22 at 08:39
  • 1
    it's true, I got a little carried away in this story, :( and it's also true that I always question the probity of certain test methods. -- It's also true that testing a display speed for only 100 lines to display doesn't really make sense. I also don't think that wanting to display a million rows at a time is a good idea, it would be an ergonomic nonsense. – Mister Jojo Dec 25 '22 at 16:09