Skip to content

Instantly share code, notes, and snippets.

@ebidel
Last active October 7, 2019 09:50
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ebidel/e9bcb6fc88b40fc26ed9e768f7d19961 to your computer and use it in GitHub Desktop.
Save ebidel/e9bcb6fc88b40fc26ed9e768f7d19961 to your computer and use it in GitHub Desktop.
Fastest way to create shadow DOM (.innerHTML vs. <template>)
<!doctype html>
<html>
<head>
<title>What's the fastest way to create shadow DOM</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
padding: 3em;
font-family: "Roboto", sans-serif;
line-height: 1.6;
color: #455A64;
}
h1, h2, h3 {
font-weight: 300;
}
h2 {
color: #90A4AE;
}
.label {
font-weight: 400;
}
#wrapper {
margin: 64px 0;
}
p {
font-size: 18px;
}
a {
text-decoration: none;
color: #455A64;
}
output {
margin-left: 32px;
display: block;
}
.red {
color: #F44336;
font-weight: 500;
text-align: center;
}
.winner {
color: #00C853;
}
</style>
</head>
<body>
<h1>What's the fastest way to create <a href="https://developers.google.com/web/fundamentals/primers/shadowdom/?hl=en" target="_blank">shadow DOM</a>?</h1>
<h2>create it from .innerHTML <b>OR</b> use a reusable &lt;template></h2>
<div id="wrapper">
<h3 class="label">FIRST INSTANCE</h3>
<output id="first">running...</output>
<br>
<h3 class="label">CREATING MANY INSTANCES ( <span id="runs"></span> runs ):</h3>
<output id="results">running...</output>
</div>
<p><b>ANALYSIS</b>: <code>&lt;template></code> is cheaper</u>. When creating a single instance using a template is most always cheaper. When creating many instances of a component, it is always cheaper.
That's because paying the parsing cost once. Creating shadow DOM by <code>.innerHTML</code>'ing a string means the string needs to be re-parsed for every instance of the component that's created.</p>
<p><b>BACKGROUND</b>: Many people want to neglict <a href="http://www.html5rocks.com/en/tutorials/webcomponents/imports/" target="_blank">HTML Imports</a> for
defining web components, and instead, use pure JS. An advantage of <code>&lt;template></code> is that you can declare your component's markup ahead of time, have the browser parse it
into DOM, and reuse it over and over again.</p>
<p style="text-align:center;">
( <a href="https://gist.github.com/ebidel/e9bcb6fc88b40fc26ed9e768f7d19961">source on github</a> )
</p>
<template id="t">
<style>
:host {
display: inline-block;
width: 650px;
font-family: 'Roboto Slab';
contain: content;
}
:host([background]) {
background: var(--background-color, #9E9E9E);
border-radius: 10px;
padding: 10px;
}
#panels {
box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
background: white;
border-radius: 3px;
padding: 16px;
height: 250px;
overflow: auto;
}
#tabs {
display: inline-flex;
-webkit-user-select: none;
user-select: none;
}
#tabs slot {
display: inline-flex; /* Safari bug. Treats <slot> as a parent */
}
/* Safari does not support #id prefixes on ::slotted
See https://bugs.webkit.org/show_bug.cgi?id=160538 */
#tabs ::slotted(*) {
font: 400 16px/22px 'Roboto';
padding: 16px 8px;
margin: 0;
text-align: center;
width: 100px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
cursor: pointer;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
background: linear-gradient(#fafafa, #eee);
border: none; /* if the user users a <button> */
}
#tabs ::slotted([aria-selected="true"]) {
font-weight: 600;
background: white;
box-shadow: none;
}
#tabs ::slotted(:focus) {
z-index: 1; /* make sure focus ring doesn't get buried */
}
#panels ::slotted([aria-hidden="true"]) {
display: none;
}
</style>
<div id="tabs">
<slot id="tabsSlot" name="title"></slot>
</div>
<div id="panels">
<slot id="panelsSlot"></slot>
</div>
</template>
<script>
(function() {
'use strict';
const NUMRUNS = 10000;
const first = document.querySelector('#first');
const results = document.querySelector('#results');
const wrapper = document.querySelector('#wrapper');
const template = document.querySelector('#t');
const lightDOM = `
<button slot="title">Tab 1</button>
<button slot="title" selected>Tab 2</button>
<button slot="title">Tab 3</button>
<section>content panel 1</section>
<section>content panel 2</section>
<section>content panel 3</section>`;
const templateHTML = t.innerHTML;
if (!document.body.attachShadow) {
wrapper.innerHTML = '<p class="red">> > Shadow DOM v1 is not supported in your browser. ' +
'Try Chrome 53+, Opera, or Safari 10 or TP. &lt; &lt;</p>';
return;
}
window.testFromTemplate = function() {
const tabs = document.createElement('fancy-tabs');
// tabs.setAttribute('background');
tabs.innerHTML = lightDOM;
tabs.attachShadow({mode: 'open'}).appendChild(t.content.cloneNode(true));
}
window.testFromString = function() {
const tabs = document.createElement('fancy-tabs');
// tabs.setAttribute('background');
tabs.innerHTML = lightDOM;
tabs.attachShadow({mode: 'open'}).innerHTML = templateHTML;
}
let TESTS = [
{title: 'from .innerHTML', test: 'testFromString', runs: []},
{title: 'from &lt;template>', test: 'testFromTemplate', runs: []}
];
TESTS.forEach(function(test) {
let func = window[test.test];
for (let i = 0 ; i < NUMRUNS; ++i) {
let start = performance.now();
func();
test.runs.push(performance.now() - start);
}
});
function calcMedian(values) {
values.sort((a, b) => a - b);
let lowMiddle = Math.floor((values.length - 1) / 2);
let highMiddle = Math.ceil((values.length - 1) / 2);
let median = (values[lowMiddle] + values[highMiddle]) / 2;
return median;
}
// Should come before subsequent runs b/c array is sorted in latter.
first.innerHTML = '';
let currFastestIdx = 0;
TESTS.forEach(function(test, i) {
let run = test.runs[0];
if (run < TESTS[currFastestIdx].runs[0]) {
currFastestIdx = i;
}
first.innerHTML += `<h3>${test.title}: ${run.toFixed(3)} ms</h3>`;
});
let firstInstanceWinner = first.querySelector(`h3:nth-of-type(${currFastestIdx + 1})`);
firstInstanceWinner.textContent += ' ✓';
firstInstanceWinner.classList.add('winner');
document.querySelector('#runs').textContent = NUMRUNS;
results.innerHTML = '';
currFastestIdx = 0;
TESTS.forEach(function(test) {
let sum = test.runs.reduce((prev, curr) => curr + prev, 0);
test.mean = (sum / test.runs.length).toFixed(3);
test.median = calcMedian(test.runs).toFixed(3);
results.innerHTML += `<h3>${test.title}: ${test.median} ms</h3>`;
});
currFastestIdx = TESTS[0].median < TESTS[1].median ? 0 : 1;
let manyInstancesWinner = results.querySelector(`h3:nth-of-type(${currFastestIdx + 1})`);
manyInstancesWinner.textContent += ' ✓';
manyInstancesWinner.classList.add('winner');
})();
</script>
</body>
</html>
@dotproto
Copy link

Try Chrome 53+, Opera, or Safari 10 or TP

What's TP?

@MattyBalaam
Copy link

Technical Preview.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment