Skip to content

Instantly share code, notes, and snippets.

@dfkaye
Last active December 12, 2023 22:20
Show Gist options
  • Save dfkaye/1c4068e05b5d891a394d8c97fbe87684 to your computer and use it in GitHub Desktop.
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?
// 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