Last active
December 12, 2023 22:19
-
-
Save dfkaye/6ceef75ee61892428ef09b3b67138cd5 to your computer and use it in GitHub Desktop.
DOM mutation benchmark: How long does it take to update the DOM?
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 11 December 2023 | |
// DOM mutation benchmark | |
// How long does it take to update the DOM? | |
// Continued by "How long does it take to update the Shadow DOM?" gist at | |
// https://gist.github.com/dfkaye/1c4068e05b5d891a394d8c97fbe87684 | |
// When it comes to those fastest DOM mutation framework benchmarks, ask how | |
// many elements are modified, which or how many attributes, especially the | |
// style attribute, and which browsers struggle with attribute, style, or | |
// child node insertion, mutation, and deletion. | |
// In this test we create a table with a tbody, 1000 rows (<tr>), where each row | |
// contains 4 cells (<td>), and perform updates on all 4000 cells each test. | |
// Unofficial Windows 10 Dell Inspiron Laptop results: | |
// 1. Firefox is fastest across the board. | |
// 2. Edge struggles with child node append, replace, remove. | |
// 3. Chrome struggles with attributes. | |
// 4. Chrome and Edge blow up on appending innerText (25 seconds), whereas FF | |
// runs in 189ms. tl;dr? Avoid innerText. | |
/** parser **/ | |
// Chrome's developer-hostile "trusted HTML" policy introduced in April or May | |
// 2023 requires this hack in order to run in the browser console. | |
var CREATE_HTML = (str) => { return String(str).trim(); }; | |
var { brand } = (navigator.userAgentData || { brands: [] }).brands.filter(_ => { | |
return !/not|brand|chromium/.test(_.brand.toLowerCase()) | |
})[0] || { brand: "" }; | |
var POLICY = /google chrome|microsoft edge/.test(brand.toLowerCase()) | |
? 'sanitize-inner-html' | |
: /opera/.test(brand.toLowerCase()) | |
? 'sanitized-policy' | |
: ''; | |
if (typeof SANITIZE == 'undefined') { | |
SANITIZE = typeof Object(window.trustedTypes).createPolicy == 'function' | |
? window.trustedTypes.createPolicy(POLICY, { | |
createHTML: CREATE_HTML | |
}) | |
: { | |
createHTML: CREATE_HTML | |
}; | |
} | |
var PARSER = new DOMParser(); | |
// parseHTML(html) returns a body element rather than the first (ideally only) | |
// element child of the body. A web component based on a single element could | |
// figure this out, but for more general uses, programmers should specify such | |
// an element as the only child that contains everything else, then extract that | |
// from the body... | |
function parseHTML(html) { | |
return PARSER.parseFromString(SANITIZE.createHTML(html), 'text/html').body; | |
}; | |
/* fixture */ | |
var TR = document.createElement('TR'); | |
TR.innerHTML = SANITIZE.createHTML( | |
`<td></td><td></td><td></td><td></td>` | |
); | |
console.time("create-table"); | |
var ROW_COUNT = 1000; | |
var TABLE = document.createElement('TABLE'); | |
TABLE.style = "color: brown; background-color: green; border-spacing: 2;" | |
var TBODY = TABLE.appendChild(document.createElement('TBODY')); | |
Array.from({ length: ROW_COUNT }).forEach(function (_, i) { | |
var ROW = TR.cloneNode(true); | |
ROW.setAttribute("data-class", `row-${i + 1}`) | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.textContent = `${i}.${j}`; | |
TD.style = "color: fuchsia; background-color: goldenrod;" | |
TD.classList.add(`TD-${j}`); | |
}); | |
TBODY.appendChild(ROW); | |
}); | |
document.body.replaceChildren( TABLE ); | |
console.timeEnd("create-table"); | |
/* timing tests */ | |
var timings = new Map(); | |
~(function SetAttributeTest(name) { | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.setAttribute("name", `${j}.${i}`); | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("set-attribute-test"); | |
~(function RemoveAttributeTest(name) { | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.removeAttribute("name"); | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("remove-attribute-test"); | |
~(function SetTwoAttributesTest(name) { | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.setAttribute("name", `${j}.${i}`); | |
TD.setAttribute("id", `${j}.${i}`); | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("set-two-attributes-test"); | |
~(function RemoveTwoAttributesTest(name) { | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.removeAttribute("name"); | |
TD.removeAttribute("id"); | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("remove-two-attributes-test"); | |
~(function RemoveUndefinedAttributeTest(name) { | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.removeAttribute("undefined"); | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("remove-undefined-attribute-test"); | |
~(function SetPropertyTest(name) { | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.name = `${j}.${i}`; | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("set-property-test"); | |
~(function SetStylePropertyTest(name) { | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.style = `color: green; background-color: aqua;`; | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("set-style-property-test"); | |
~(function SetStyleBorderPropertyTest(name) { | |
console.time(name); | |
var start = performance.now(name); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.style = `border: 1px solid fuchsia;`; | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("set-style-border-property-test"); | |
~(function AddClassListItemTest(name) { | |
console.time("add-class-list-item-test"); | |
var start = performance.now(name); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.classList.add(`TD-${i}-${j}`); | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("add-class-list-item-test"); | |
~(function RemoveClassListItemTest(name) { | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.classList.remove('chunk'); | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("remove-class-list-item-test"); | |
~(function AddDatasetItemTest(name) { | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.dataset.test = `TD-${i}-${j}`; | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("add-dataset-item-test"); | |
~(function DeleteDatasetItemTest(name) { | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
delete TD.dataset.test; // = `TD-${i}-${j}`; | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("delete-dataset-item-test"); | |
~(function AddChildNodeTest(name) { | |
var DIV = document.createElement("DIV"); | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.appendChild(DIV.cloneNode()); | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("add-childNode-test"); | |
~(function ReplaceChildNodeTest(name) { | |
var node = document.createElement("P"); | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.replaceChild(node.cloneNode(), TD.firstElementChild); | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("replace-childNode-test"); | |
~(function RemoveChildNodeTest(name) { | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.removeChild(TD.firstElementChild); | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("remove-childNode-test"); | |
~(function SetTextContentTest(name) { | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.textContent = `TD-${j}-${i}`; | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("set-textContent-test"); | |
~(function AppendTextContentTest(name) { | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.textContent += `-appended`; | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("append-textContent-test"); | |
~(function SetNodeValueTest(name) { | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.firstChild.nodeValue = `TD-${j}-${i}`; | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("set-nodeValue-test"); | |
~(function AppendNodeValueTest(name) { | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.firstChild.nodeValue += `-appended`; | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("append-nodeValue-test"); | |
~(function RemoveNodeValueTest(name) { | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.firstChild.nodeValue = ``; | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("remove-nodeValue-test"); | |
~(function RemoveTextContentTest(name) { | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.textContent = ""; | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("remove-text-content-test"); | |
~(function SetInnerTextTest(name) { | |
console.time("set-innerText-test"); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.innerText = `TD-${j}-${i}-innerText`; | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("set-innerText-test"); | |
~(function AppendInnerTextTest(name) { | |
console.time(name); | |
console.log(` | |
append-innerText explodes in Chrome and Edge, taking 25 seconds, | |
whereas in Firefox it takes 189ms. | |
`); | |
// var start = performance.now(); | |
// TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
// ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
// TD.innerText += `-appended`; | |
// }); | |
// }); | |
// timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("append-innerText-test"); | |
~(function ChildInputTest() { | |
console.group("Child Input Tests") | |
var INPUT = document.createElement("INPUT"); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.replaceChildren(INPUT.cloneNode(true)); | |
}); | |
}); | |
~(function (name) { | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.firstElementChild.value = `INPUT-{j}-{i}`; | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("child-input-value-test"); | |
~(function (name) { | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.firstElementChild.removeAttribute("value"); | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("child-input-remove-attribute-test"); | |
~(function (name) { | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.firstElementChild.style = "color: red; border: 1px sold red;"; | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("child-input-set-style-test"); | |
~(function (name) { | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.firstElementChild.style.borderColor = "aqua"; | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("child-input-replace-style-test"); | |
~(function (name) { | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.firstElementChild.removeAttribute("style"); | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("child-input-remove-style-test"); | |
console.groupEnd("Child Input Tests"); | |
})(); | |
~(function DisabledReadonlyInputTest() { | |
console.group("Disabled-Readonly Input Tests") | |
var INPUT = document.createElement("INPUT"); | |
INPUT.style.color = "blue"; | |
INPUT.style.fontStyle = "italic"; | |
INPUT.setAttribute("value", "abcdef"); | |
INPUT.setAttribute("disabled", true); | |
INPUT.setAttribute("readonly", false); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.replaceChildren(INPUT.cloneNode(true)); | |
}); | |
}); | |
~(function (name) { | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.removeAttribute("disabled"); | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("child-input-remove-disabled-attribute-test"); | |
~(function (name) { | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.disabled = true; | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("child-input-set-disabled-property-test"); | |
~(function (name) { | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.setAttribute("readonly", true); | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("child-input-readonly-attribute-test"); | |
~(function (name) { | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.setAttribute("readonly", false); | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("child-input-readonly-attribute-false-test"); | |
~(function (name) { | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.removeAttribute("readonly", false); | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("child-input-remove-readonly-attribute-test"); | |
~(function (name) { | |
console.time(name); | |
var start = performance.now(); | |
TBODY.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.readonly = false; | |
}); | |
}); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("child-input-readonly-property-test"); | |
})(); | |
~(function ReplaceBigTableTest() { | |
var CLONE = TBODY.cloneNode(true); | |
CLONE.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.setAttribute("class", "clown"); | |
TD.style = "color: red; background: gold;"; | |
TD.textContent = `C-${i}-${j}`; | |
}); | |
}); | |
var TBODY_CLONE = TBODY.cloneNode(true); | |
TBODY_CLONE.querySelectorAll("TR").forEach(function (ROW, i) { | |
ROW.querySelectorAll("TD").forEach(function (TD, j) { | |
TD.removeAttribute("class"); | |
TD.removeAttribute("style"); | |
TD.textContent = `C-${i}-${j}`; | |
}); | |
}); | |
TABLE.replaceChildren(TBODY_CLONE); | |
~(function (name) { | |
console.time(name); | |
var start = performance.now(); | |
TBODY_CLONE.replaceChildren(...CLONE.childNodes); | |
timings.set(name, performance.now() - start + "ms"); | |
console.timeEnd(name); | |
})("replace-tbody-children-test"); | |
console.groupEnd("Disabled-Readonly Input Tests") | |
})(); | |
~(function () { | |
console.log(" **** **** **** **** "); | |
console.group("report") | |
console.info(TABLE.querySelectorAll("TD").length + " <TD> elements"); | |
for (var [name, time] of timings) { | |
var msg = `%c${name}: ${time}`; | |
var style = `background-color: mintcream; color: darkgreen;` | |
if (parseInt(time) > 10) { | |
msg = `%c> ${name}: ${time} <`; | |
style = `background-color: mintgreen; font-weight: bold;`; | |
} | |
if (parseInt(time) > 20) { | |
msg = `%c>> ${name}: ${time} <<`; | |
style = `background-color: lightgoldenrodyellow; font-weight: bold;`; | |
} | |
if (parseInt(time) > 30) { | |
msg = `%c>>> ${name}: ${time} <<<`; | |
style = `background-color: yellow; color: darkred; font-weight: bold;`; | |
} | |
if (parseInt(time) > 40) { | |
msg = `%c>>>> ${name}: ${time} <<<<`; | |
style = `background-color: pink; color: darkred; font-weight: bold;`; | |
} | |
console.log.apply(console, [msg, style]); | |
} | |
console.groupEnd("report"); | |
})(); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment