The words Under construction in black text on a yellow background with diagonal black stipes surrounding it
I'm in the process of moving my site. It's still a work in progress. Please excuse the mess and broken links.

Comparing innerHTML And appendChild Page Rendering Speed

TODO: Pull subtitle into page object
Test
(ms)

(The tests are not async. The page will be unresponsive for a bit. Up to 10 seconds on my machine)

Notes

The Tests

This is the code for each test

Testing Ground

This is the target area where the tests do their rendering

Debugging Stuff

I'm moving stuff around right now. All this below is helping me figure out where to put stuff

        -- title

Comparing innerHTML And appendChild Page Rendering Speed

I'm building an <<link|ASCII avatar|https://www.alanwsmith.com/asciibear/>>
for my <<link|Twitch stream|https://www.twitch.tv/theidofalan>>. 
Think VTuber, but 2 dimensional. 
The avatar is made from a table that lets me set styles
for each character individually. I built this page 
to compare rendering methods.

I've been using the "Table appendChild (Text)" approach. Based off 
these results I'm switching to a _span__ based approach. 

You can run the tests yourself here. More details and the test
code are below.

-- html



<div id="testResultsWrapper">
<div id="testResults">
<div>Test</div><div>(ms)</div>
</div>
</div>

<div><button id="runTestButton">Run Tests</button>
<br>
(The tests are not async. The page will be unresponsive for a bit. Up to 10 seconds on my machine)
</div>


-- notes
-- title: Notes

- Each test produces a 40x60 grid of individual 
characters either in a table cell or a span 
tag. That results in 2,400 changes per update 
(which is based off the size of my avatar)

- Each render function is called 50 times

- Two classes are applied to each position 
representing a foreground and background
color. 

- A "." character is added to each position. 

- Each test name includes either "(Text)" or
"(HTML)" at the end. This indicates the method
that was used to insert the "." characters into
their positions (i.e. either "innerText" or 
"innerHTML"). 

- The "appendChild" tests assemble a single table
or div in the function then make a single "appendChild"
call to the parent element in the DOM to insert it
at the end of the function

- The appendChildren tests insert the table or div
at the start of the function call. Elements are then
added into the DOM underneath it

- The innerHTML tests create a single string of HTML
as text and insert it into the parent element via
innerHTML

- The "Cell Update" tests produce a single table or grid
of spans before the tests are run. Each test run
clears the contents from the grid while leaving it
intact and then refills the content 

- Using requestAnimationFrame might have some impact on 
the results. Checking that is left as an exercise for
the reader

- The source code for the tests and the output 
testing ground are below


-- h3

The Tests

This is the code for each test


-- script
-- title: Table Append Child Text
-- show

const runTableAppendChildText = (rows, cols) => {
  const newTable = document.createElement("table")
  for (let r = 0; r < rows; r ++) {
    const newTr = document.createElement("tr")
    for (let c = 0; c < cols; c ++) {
      const newTd = document.createElement("td")
      newTd.classList.add("foregroundColor")
      newTd.classList.add("backgroundColor")
      newTd.innerText = "."
      newTr.appendChild(newTd)
    }
    newTable.appendChild(newTr)
  }
  tableAppendChildText.children[0].remove()
  tableAppendChildText.appendChild(newTable)
}


-- script
-- title: Table Append Children Text
-- show

const runTableAppendChildrenText = (rows, cols) => {
  const newTable = document.createElement("table")
  tableAppendChildrenText.children[0].remove()
  tableAppendChildrenText.appendChild(newTable)
  for (let r = 0; r < rows; r ++) {
    const newTr = document.createElement("tr")
    for (let c = 0; c < cols; c ++) {
      const newTd = document.createElement("td")
      newTd.classList.add("foregroundColor")
      newTd.classList.add("backgroundColor")
      newTd.innerText = "."
      newTr.appendChild(newTd)
    }
    newTable.appendChild(newTr)
  }
}

-- script
-- title: Table innerHTML Text
-- show

const runTableInnerHTMLText = (rows, cols) => {
  let tableString = "<table>"
  for (let r = 0; r < rows; r ++) {
    tableString += "<tr>"
    for (let c = 0; c < cols; c ++) {
      tableString += `<td class="foregroundColor backgroundColor">.</td>`
    }
    tableString += "</tr>"
  }
  tableString += "</table>"
  tableInnerHTMLText.children[0].remove()
  tableInnerHTMLText.innerHTML = tableString
}

-- script
-- show
-- title: Table Cell Update Text

const tableUpdateCellsGridText = []

const prepTableUpdateCellsText = (rows, cols) => {
  const newTable = document.createElement("table")
  for (let r = 0; r < rows; r ++) {
    const newRow = []
    const newTr = document.createElement("tr")
    for (let c = 0; c < cols; c ++) {
      const newTd = document.createElement("td")
      newTd.classList.add("foregroundColor")
      newRow.push(newTd)
      newTr.appendChild(newTd)
    }
    tableUpdateCellsGridText.push(newRow)
    newTable.appendChild(newTr)
  }
  tableUpdateCellsText.appendChild(newTable)
}

const runTableUpdateCellsText = (rows, cols) => {
  for (let r = 0; r < rows; r ++) {
    for (let c = 0; c < cols; c ++) {
      tableUpdateCellsGridText[r][c].classList = ""
      tableUpdateCellsGridText[r][c].classList.add("foregroundColor")
      tableUpdateCellsGridText[r][c].classList.add("backgroundColor")
      tableUpdateCellsGridText[r][c].innerText = "."
    }
  }
}



-- script
-- title: Span Append Child Text
-- show

const runSpanAppendChildText = (rows, cols) => {
  const newDiv = document.createElement("div")
  newDiv.classList.add("spanGrid")
  for (let r = 0; r < rows; r ++) {
    for (let c = 0; c < cols; c ++) {
      const newSpan = document.createElement("span")
      newSpan.classList.add("spanForegroundColor")
      newSpan.classList.add("backgroundColor")
      newSpan.innerText = "."
      newDiv.appendChild(newSpan)
    }
  }
  spanAppendChildText.children[0].remove()
  spanAppendChildText.appendChild(newDiv)
}


-- script
-- title: Span Append Children Text
-- show

const runSpanAppendChildrenText = (rows, cols) => {
  const newDiv = document.createElement("div")
  spanAppendChildrenText.children[0].remove()
  spanAppendChildrenText.appendChild(newDiv)
  newDiv.classList.add("spanGrid")
  for (let r = 0; r < rows; r ++) {
    for (let c = 0; c < cols; c ++) {
      const newSpan = document.createElement("span")
      newSpan.classList.add("spanForegroundColor")
      newSpan.classList.add("backgroundColor")
      newSpan.innerText = "."
      newDiv.appendChild(newSpan)
    }
  }
}


-- script
-- title: Span innerHTML Text
-- show

const runSpanInnerHTMLText = (rows, cols) => {
  let outputString = `<div class="spanGrid">`
  for (let r = 0; r < rows; r ++) {
    for (let c = 0; c < cols; c ++) {
      outputString += `<span class="spanForegroundColor backgroundColor">.</span>`
    }
  }
  outputString += `</div>`
  spanInnerHTMLText.children[0].remove()
  spanInnerHTMLText.innerHTML = outputString
}


-- script
-- show
-- title: Span Update Text

const prepSpanUpdateCellsText = (rows, cols) => {
  for (let r = 0; r < rows; r ++) {
    for (let c = 0; c < cols; c ++) {
      const newSpan = document.createElement("span")
      spanUpdateCellsText.appendChild(newSpan)
    }
  }
}

const runSpanUpdateCellsText = (rows, cols) => {
  let counter = 0
  for (let r = 0; r < rows; r ++) {
    for (let c = 0; c < cols; c ++) {
      const targetSpan = spanUpdateCellsText.children[counter]
      targetSpan.classList = ""
      targetSpan.classList.add("spanForegroundColor")
      targetSpan.classList.add("backgroundColor")
      targetSpan.innerText = "."
      counter += 1
    }
  }
}


-- script
-- title: Table Append Child HTML
-- show

const runTableAppendChildHTML = (rows, cols) => {
  const newTable = document.createElement("table")
  for (let r = 0; r < rows; r ++) {
    const newTr = document.createElement("tr")
    for (let c = 0; c < cols; c ++) {
      const newTd = document.createElement("td")
      newTd.classList.add("foregroundColor")
      newTd.classList.add("backgroundColor")
      newTd.innerHTML = "."
      newTr.appendChild(newTd)
    }
    newTable.appendChild(newTr)
  }
  tableAppendChildHTML.children[0].remove()
  tableAppendChildHTML.appendChild(newTable)
}


-- script
-- title: Table Append Children HTML
-- show

const runTableAppendChildrenHTML = (rows, cols) => {
  const newTable = document.createElement("table")
  tableAppendChildrenHTML.children[0].remove()
  tableAppendChildrenHTML.appendChild(newTable)
  for (let r = 0; r < rows; r ++) {
    const newTr = document.createElement("tr")
    for (let c = 0; c < cols; c ++) {
      const newTd = document.createElement("td")
      newTd.classList.add("foregroundColor")
      newTd.classList.add("backgroundColor")
      newTd.innerHTML = "."
      newTr.appendChild(newTd)
    }
    newTable.appendChild(newTr)
  }
}

-- script
-- title: Table innerHTML HTML
-- show

const runTableInnerHTMLHTML = (rows, cols) => {
  let tableString = "<table>"
  for (let r = 0; r < rows; r ++) {
    tableString += "<tr>"
    for (let c = 0; c < cols; c ++) {
      tableString += `<td class="foregroundColor backgroundColor">.</td>`
    }
    tableString += "</tr>"
  }
  tableString += "</table>"
  tableInnerHTMLHTML.children[0].remove()
  tableInnerHTMLHTML.innerHTML = tableString
}

-- script
-- show
-- title: Table Cell Update HTML

const tableUpdateCellsGridHTML = []

const prepTableUpdateCellsHTML = (rows, cols) => {
  const newTable = document.createElement("table")
  for (let r = 0; r < rows; r ++) {
    const newRow = []
    const newTr = document.createElement("tr")
    for (let c = 0; c < cols; c ++) {
      const newTd = document.createElement("td")
      newTd.classList.add("foregroundColor")
      newRow.push(newTd)
      newTr.appendChild(newTd)
    }
    tableUpdateCellsGridHTML.push(newRow)
    newTable.appendChild(newTr)
  }
  tableUpdateCellsHTML.appendChild(newTable)
}

const runTableUpdateCellsHTML = (rows, cols) => {
  for (let r = 0; r < rows; r ++) {
    for (let c = 0; c < cols; c ++) {
      tableUpdateCellsGridHTML[r][c].classList = ""
      tableUpdateCellsGridHTML[r][c].classList.add("foregroundColor")
      tableUpdateCellsGridHTML[r][c].classList.add("backgroundColor")
      tableUpdateCellsGridHTML[r][c].innerHTML = "."
    }
  }
}



-- script
-- title: Span Append Child HTML
-- show

const runSpanAppendChildHTML = (rows, cols) => {
  const newDiv = document.createElement("div")
  newDiv.classList.add("spanGrid")
  for (let r = 0; r < rows; r ++) {
    for (let c = 0; c < cols; c ++) {
      const newSpan = document.createElement("span")
      newSpan.classList.add("spanForegroundColor")
      newSpan.classList.add("backgroundColor")
      newSpan.innerHTML = "."
      newDiv.appendChild(newSpan)
    }
  }
  spanAppendChildHTML.children[0].remove()
  spanAppendChildHTML.appendChild(newDiv)
}


-- script
-- title: Span Append Children HTML
-- show

const runSpanAppendChildrenHTML = (rows, cols) => {
  const newDiv = document.createElement("div")
  spanAppendChildrenHTML.children[0].remove()
  spanAppendChildrenHTML.appendChild(newDiv)
  newDiv.classList.add("spanGrid")
  for (let r = 0; r < rows; r ++) {
    for (let c = 0; c < cols; c ++) {
      const newSpan = document.createElement("span")
      newSpan.classList.add("spanForegroundColor")
      newSpan.classList.add("backgroundColor")
      newSpan.innerHTML = "."
      newDiv.appendChild(newSpan)
    }
  }
}


-- script
-- title: Span innerHTML HTML
-- show

const runSpanInnerHTMLHTML = (rows, cols) => {
  let outputString = `<div class="spanGrid">`
  for (let r = 0; r < rows; r ++) {
    for (let c = 0; c < cols; c ++) {
      outputString += `<span class="spanForegroundColor backgroundColor">.</span>`
    }
  }
  outputString += `</div>`
  spanInnerHTMLHTML.children[0].remove()
  spanInnerHTMLHTML.innerHTML = outputString
}


-- script
-- show
-- title: Span Update HTML

const prepSpanUpdateCellsHTML = (rows, cols) => {
  for (let r = 0; r < rows; r ++) {
    for (let c = 0; c < cols; c ++) {
      const newSpan = document.createElement("span")
      spanUpdateCellsHTML.appendChild(newSpan)
    }
  }
}

const runSpanUpdateCellsHTML = (rows, cols) => {
  let counter = 0
  for (let r = 0; r < rows; r ++) {
    for (let c = 0; c < cols; c ++) {
      const targetSpan = spanUpdateCellsHTML.children[counter]
      targetSpan.classList = ""
      targetSpan.classList.add("spanForegroundColor")
      targetSpan.classList.add("backgroundColor")
      targetSpan.innerHTML = "."
      counter += 1
    }
  }
}



-- script
-- show
-- title: Test Runner

const runTests = (rows, cols) => {
  const runs = 50
  const results = []
  const tests = [
    { name: "Table Append Child (Text)", func: runTableAppendChildText },
    { name: "Table Append Children (Text)", func: runTableAppendChildrenText },
    { name: "Table InnerHTML (Text)", func: runTableInnerHTMLText },
    { name: "Table Update Cells (Text)", func: runTableUpdateCellsText },
    { name: "Span Append Child Text", func: runSpanAppendChildText },
    { name: "Span Append Children Text", func: runSpanAppendChildrenText },
    { name: "Span Inner HTML Text", func: runSpanInnerHTMLText },
    { name: "Span Update Cells Text", func: runSpanUpdateCellsText },
    { name: "Table Append Child HTML", func: runTableAppendChildHTML },
    { name: "Table Append Children HTML", func: runTableAppendChildrenHTML },
    { name: "Table Inner HTML HTML", func: runTableInnerHTMLHTML },
    { name: "Table Update Cells HTML", func: runTableUpdateCellsHTML },
    { name: "Span Append Child HTML", func: runSpanAppendChildHTML },
    { name: "Span Append Children HTML", func: runSpanAppendChildrenHTML },
    { name: "Span Inner HTML HTML", func: runSpanInnerHTMLHTML },
    { name: "Span Update Cells HTML", func: runSpanUpdateCellsHTML },
  ]
  tests.forEach((test) => {
    const resultValue = testRunner(test.func, rows, cols, runs) / runs
    results.push(
      {name: test.name, value: resultValue, display: resultValue.toFixed(2)}
    )
  })

  results.sort((a, b) => {
    return a.value - b.value
  })

  results.forEach((r) => {
    const name = document.createElement("div")
    name.innerText = r.name
    const display = document.createElement("div")
    display.innerText = r.display
    testResults.appendChild(name)
    testResults.appendChild(display)
  })

  console.log(results)
}

const testRunner = (testFunction, rows, cols, runs) => {
  const startTime = new Date()
  for (let t = 0; t < runs; t++) {
    testFunction(rows, cols)
  }
  const endTime = new Date()
  return endTime - startTime
}

document.addEventListener(
  "DOMContentLoaded",
  () => { 
    const rows = 40
    const cols = 60
    prepSpanUpdateCellsText(rows, cols)
    prepTableUpdateCellsText(rows, cols)
    prepSpanUpdateCellsHTML(rows, cols)
    prepTableUpdateCellsHTML(rows, cols)
    runTestButton.addEventListener("click", () => {
      runTests(rows, cols) 
    })
  }
)

-- h3

Testing Ground

This is the target area where the tests
do their rendering

-- html 



<div id="examples">
  <div>
    <div id="tableAppendChildText"><div></div></div>
  </div>
  <div>
    <div id="tableAppendChildrenText"><div></div></div>
  </div>
  <div>
    <div id="tableInnerHTMLText"><div></div></div>
  </div>
  <div>
    <div id="tableUpdateCellsText"></div>
  </div>
  <div>
    <div id="spanAppendChildText"><div></div></div>
  </div>
  <div>
    <div id="spanAppendChildrenText"><div></div></div>
  </div>
  <div>
    <div id="spanInnerHTMLText"><div></div></div>
  </div>
  <div>
    <div id="spanUpdateCellsText" class="spanGrid"></div>
  </div>
  <div>
    <div id="tableAppendChildHTML"><div></div></div>
  </div>
  <div>
    <div id="tableAppendChildrenHTML"><div></div></div>
  </div>
  <div>
    <div id="tableInnerHTMLHTML"><div></div></div>
  </div>
  <div>
    <div id="tableUpdateCellsHTML"></div>
  </div>
  <div>
    <div id="spanAppendChildHTML"><div></div></div>
  </div>
  <div>
    <div id="spanAppendChildrenHTML"><div></div></div>
  </div>
  <div>
    <div id="spanInnerHTMLHTML"><div></div></div>
  </div>
  <div>
    <div id="spanUpdateCellsHTML" class="spanGrid"></div>
  </div>
</div>


-- css

#testResultsWrapper {
  border: var(--main-border-secondary-base);
  border-radius: var(--main-border-radius-base);
  padding: var(--main-padding-base);
}

#examples {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr 1fr;
}

table {
  border-collapse: collapse;
}

.foregroundColor {
  font-size: 0.1rem;
  color: gold;
}

.spanForegroundColor {
  font-size: 0.1rem;
  color: gold;
  line-height: 0.1rem;
  height: 0.1rem;
}

.spanGrid {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr
}

#testResults {
  display: grid;
  grid-template-columns: 30ch 8ch;
  & div {
    font-family: monospace;
    text-alight: right;
  }
  
}

-- categories
-- JavaScript 

-- metadata
-- date: 2023-08-28 12:46:54
-- id: 2ucaevt7
-- site: aws
-- type: post
-- status: draft