Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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

This comment has been minimized.

Copy link

commented Aug 12, 2016

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

What's TP?

@MattyBalaam

This comment has been minimized.

Copy link

commented Aug 29, 2016

Technical Preview.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.