Skip to content

Commit

Permalink
Fix template injection for special characters.
Browse files Browse the repository at this point in the history
The following should work (and work performantly) as a result of this
change set:

```javascript
html`\
  <svg
    id="svg"
    class="<><></></>"
    xmlns="http://www.w3.org/2000/svg"
    viewBox="0 0 24 24"
    width="${ifDefined(width)}"
    height="${ifDefined(height)}">
    <circle id="circle" r="12" cx="12" cy="12"></circle>
  </svg>
```
  • Loading branch information
theengineear committed Mar 19, 2023
1 parent 92e6ed6 commit 61ded62
Show file tree
Hide file tree
Showing 6 changed files with 301 additions and 203 deletions.
19 changes: 19 additions & 0 deletions demo/performance/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
:root {
line-height: 20px;
}

.label {
font-weight: bold;
}

.output {
height: calc(20px * 3);
}

p {
width: 70ch;
}

p:first-of-type {
margin-top: 2em;
}
52 changes: 41 additions & 11 deletions demo/performance/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,50 @@
<html>
<head>
<meta charset="UTF-8">
<script type="module" src="./index.js"></script>
<link rel="stylesheet" href="./index.css">
</head>
<body>
<h1>default</h1>
<div id="default"></div>
<h1>lit html</h1>
<div id="lit-html"></div>
<h1>µhtml</h1>
<div id="uhtml"></div>
<div>
<div class="label">default</div>
<pre id="default" class="output"></pre>
</div>
<div>
<div class="label">lit html</div>
<pre id="lit-html" class="output"></pre>
</div>
<div>
<div class="label">µhtml</div>
<pre id="uhtml" class="output"></pre>
</div>
<p>
This tests the performance of <code>html</code> and <code>render</code>.
By testing <em>only</em> these two functions, we isolate time spent by the
templating engine from time spent by the element base class.
</p>
<p>
The term &ldquo;inject&rdquo; refers to the process of taking an array
of template strings, injecting special markup strings, instantiating a
<code>&lt;template&gt;</code> element, mapping DOM elements based on the
special markup previously injected, cloning that template element, and
finally rendering it within a container element. Injection happens only
<em>once per template function declaration</em>. This is the most
time-consuming step of the process, but it also is only typically needed
once per element base class definition.
</p>
<p>
The term &ldquo;initial&rdquo; refers to the process of taking a
template that&rsquo;s <em>already</em> been injected and rendering it into
a new container element. For example, if you render multiple elements in
the page, the templating function is likely shared &mdash; this means that
the engine can skip the <em>injection</em> phase altogether. This happens
whenever a new element of the same type is created (i.e., the same element
appearing in a list over-and-over again).
</p>
<p>
The elements in this test all share a common base element. The only
difference is that they override the templating engine. The test consists
of two measurements &mdash; time to initialize and time to update.
The term &ldquo;update&rdquo; refers to the process of changing the values
which are interpolated into a given template. This is the most common
thing the engine needs to do.
</p>
<script type="module" src="./performance.js"></script>
<script type="module" src="./index.js"></script>
</body>
</html>
148 changes: 80 additions & 68 deletions demo/performance/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,84 +2,96 @@ import XElement from '../../x-element.js';
import { html as litHtmlHtml, render as litHtmlRender } from 'https://unpkg.com/lit-html/lit-html.js?module';
import { render as uhtmlRender, html as uhtmlHtml } from 'https://unpkg.com/uhtml?module';

class DefaultPerformanceElement extends XElement {
static get properties() {
return {
base: {
type: Number,
initial: 3,
},
height: {
type: Number,
initial: 4,
},
hypotenuse: {
type: Number,
input: ['base', 'height'],
compute: Math.hypot,
},
valid: {
type: Boolean,
input: ['hypotenuse'],
compute: hypotenuse => !!hypotenuse,
reflect: true,
},
perfect: {
type: Boolean,
input: ['hypotenuse'],
compute: hypotenuse => Number.isInteger(hypotenuse),
reflect: true,
},
title: {
type: String,
internal: true,
input: ['valid', 'perfect'],
compute: (valid, perfect) => {
return !valid
? 'This is not a triangle.'
: perfect
? 'This is a perfect triangle.'
: 'This is a triangle.';
},
},
};
}

static template(html) {
return ({ base, height, hypotenuse, title }) => html`
<style>
:host {
display: block;
}
:host(:not([valid])) {
color: red;
}
:host([perfect]) {
color: green;
}
</style>
<code base="${base}" height="${height}" .title="${title}">
Math.hypot(${base}, ${height}) = ${hypotenuse}
<code>
`;
}
}
customElements.define('default-performance', DefaultPerformanceElement);

class LitHtmlPerformanceElement extends DefaultPerformanceElement {
class LitHtmlElement extends XElement {
// Use lit-html's template engine rather than the built-in x-element engine.
static get templateEngine() {
const render = (container, template) => litHtmlRender(template, container);
const html = litHtmlHtml;
return { render, html };
}
}
customElements.define('lit-html-performance', LitHtmlPerformanceElement);

class UhtmlPerformanceElement extends DefaultPerformanceElement {
class UhtmlElement extends XElement {
// Use µhtml's template engine rather than the built-in x-element engine.
static get templateEngine() {
return { render: uhtmlRender, html: uhtmlHtml };
}
}
customElements.define('uhtml-performance', UhtmlPerformanceElement);

/*
// Reference template string (since it's hard to read as an interpolated array).
// Note that there are no special characters here so strings == strings.raw.
<div id="p1" attr="${attr}"><div id="p2" data-foo><div id="p3" data-bar="bar"><div id="${id}" boolean ?hidden="${hidden}" .title="${title}">${content1} -- ${content2}</div></div></div></div>
*/
const getStrings = () => {
const strings = ['<div id="p1" attr="', '"><div id="p2" data-foo><div id="p3" data-bar="bar"><div id="', '" boolean ?hidden="', '" .title="', '">', ' -- ', '</div></div></div></div>'];
strings.raw = ['<div id="p1" attr="', '"><div id="p2" data-foo><div id="p3" data-bar="bar"><div id="', '" boolean ?hidden="', '" .title="', '">', ' -- ', '</div></div></div></div>'];
return strings;
};
const getValues = ({ attr, id, hidden, title, content1, content2 }) => {
const values = [attr, id, hidden, title, content1, content2];
return values;
};

const run = async (output, constructor) => {
output.textContent = '';
const { render, html } = constructor.templateEngine;
const injectCount = 100000;
const initialCount = 1000000;
const updateCount = 1000000;

// Test inject performance.
await new Promise(resolve => setTimeout(resolve, 0));
let injectSum = 0;
const injectProperties = { attr: '123', id: 'foo', hidden: false, title: 'test', content1: 'AAA', content2: 'BBB' };
const injectContainer = document.createElement('div');
for (let iii = 0; iii < injectCount; iii++) {
const strings = getStrings();
const values = getValues(injectProperties);
const t0 = performance.now();
render(injectContainer, html(strings, ...values));
const t1 = performance.now();
injectSum += t1 - t0;
}
const injectAverage = `${(injectSum / injectCount * 1000).toFixed(1).padStart(6)} µs`;
output.textContent += `${output.textContent ? '\n' : ''}inject: ${injectAverage} (tested ${injectCount.toLocaleString()} times)`;

// Test initial performance.
await new Promise(resolve => setTimeout(resolve, 0));
let initialSum = 0;
const initialStrings = getStrings();
const initialProperties = { attr: '123', id: 'foo', hidden: false, title: 'test', content1: 'AAA', content2: 'BBB' };
const initialValues = getValues(initialProperties);
const initialContainer = document.createElement('div');
for (let iii = 0; iii < initialCount; iii++) {
const t0 = performance.now();
render(initialContainer, html(initialStrings, ...initialValues));
const t1 = performance.now();
initialSum += t1 - t0;
}
const initialAverage = `${(initialSum / initialCount * 1000).toFixed(1).padStart(4)} µs`;
output.textContent += `${output.textContent ? '\n' : ''}initial: ${initialAverage} (tested ${initialCount.toLocaleString()} times)`;

// Test update performance.
await new Promise(resolve => setTimeout(resolve, 0));
let updateSum = 0;
const updateStrings = getStrings();
const updateProperties = [
{ attr: '123', id: 'foo', hidden: false, title: 'test', content1: 'AAA', content2: 'BBB' },
{ attr: '456', id: 'bar', hidden: false, title: 'test', content1: 'ZZZ', content2: 'BBB' },
];
const updateContainer = document.createElement('div');
for (let iii = 0; iii < updateCount; iii++) {
const values = getValues(updateProperties[iii % 2]);
const t0 = performance.now();
render(updateContainer, html(updateStrings, ...values));
const t1 = performance.now();
updateSum += t1 - t0;
}
const updateAverage = `${(updateSum / updateCount * 1000).toFixed(1).padStart(5)} µs`;
output.textContent += `${output.textContent ? '\n' : ''}update: ${updateAverage} (tested ${updateCount.toLocaleString()} times)`;
};

await run(document.getElementById('default'), XElement);
await run(document.getElementById('lit-html'), LitHtmlElement);
await run(document.getElementById('uhtml'), UhtmlElement);
49 changes: 0 additions & 49 deletions demo/performance/performance.js

This file was deleted.

Loading

0 comments on commit 61ded62

Please sign in to comment.