Last active
December 12, 2023 22:20
-
-
Save dfkaye/1c4068e05b5d891a394d8c97fbe87684 to your computer and use it in GitHub Desktop.
Shadow DOM mutation benchmark: How long does it take to update the Shadow 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
// 12 December 2023 | |
// Shadow DOM mutation benchmark | |
// How long does it take to update the Shadow DOM? | |
// Successor to "How long does it take to update the DOM?" gist at | |
// https://gist.github.com/dfkaye/6ceef75ee61892428ef09b3b67138cd5 | |
// In this test, as before, 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. | |
// However, in this test, we create a host DIV element, create a shadow root, | |
// and attach the table to the shadow root. | |
// Findings: | |
// 1. Firefox still superior performance across all tests, plus improved child | |
// node append, replace, remove speeds (maybe 30%). | |
// 2. Edge slightly improved child node append, replace, remove (maybe 20%); | |
// slightly worse child input remove readonly attribute speed. | |
// 3. Chrome slightly improved add classlist item; slightly worse child input | |
// remove readonly attribute; significantly *worse* child input set readonly | |
// attribute to false (from 11ms to 31ms) - this could be due to coercing the | |
// attribute value from false to "false" as opposed to setting the readonly | |
// property to false. | |
/** 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 HOST = document.createElement("DIV"); | |
var TABLE = document.createElement('TABLE'); | |
var ROOT = HOST.attachShadow({ mode: "open" }); | |
ROOT.appendChild(TABLE); | |
var TBODY = TABLE.appendChild(document.createElement('TBODY')); | |
TABLE.style = "color: brown; background-color: green; border-spacing: 2;" | |
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( HOST ); | |
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); | |
// ROOT.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