Skip to content

Commit

Permalink
Improve performance of injection phase.
Browse files Browse the repository at this point in the history
Just a few changes to improve the performance of going from a
user-defined template function to a clonable template element.
  • Loading branch information
theengineear committed Mar 23, 2023
1 parent 33d0487 commit 3c73774
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 126 deletions.
6 changes: 6 additions & 0 deletions demo/performance/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@
which are interpolated into a given template. This is the most common
thing the engine needs to do.
</p>
<p>
Finally, a note on how the tests work — they are batched up and run within
animation frames to guard against any interference that might occur when
an animation frame is skipped due to the main thread being busy. This is
why the tests all take the same amount of time to complete.
</p>
<script type="module" src="./index.js"></script>
</body>
</html>
129 changes: 77 additions & 52 deletions demo/performance/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,66 +32,91 @@ const getValues = ({ attr, id, hidden, title, content1, content2 }) => {
const values = [attr, id, hidden, title, content1, content2];
return values;
};
const tick = async () => {
await new Promise(resolve => requestAnimationFrame(resolve));
};

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

// 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;
if (tests.includes('inject')) {
// Test inject performance.
await tick();
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 + slop; iii++) {
const strings = getStrings();
const values = getValues(injectProperties);
const t0 = performance.now();
render(injectContainer, html(strings, ...values));
const t1 = performance.now();
if (iii >= slop) {
injectSum += t1 - t0;
}
if (iii % 100 === 0) {
await tick();
}
}
const injectAverage = `${(injectSum / injectCount * 1000).toFixed(1).padStart(6)} µs`;
output.textContent += `${output.textContent ? '\n' : ''}inject: ${injectAverage} (tested ${injectCount.toLocaleString()} times)`;
}
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;
if (tests.includes('initial')) {
// 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 + slop; iii++) {
const t0 = performance.now();
render(initialContainer, html(initialStrings, ...initialValues));
const t1 = performance.now();
if (iii >= slop) {
initialSum += t1 - t0;
}
if (iii % 1000 === 0) {
await tick();
}
}
const initialAverage = `${(initialSum / initialCount * 1000).toFixed(1).padStart(4)} µs`;
output.textContent += `${output.textContent ? '\n' : ''}initial: ${initialAverage} (tested ${initialCount.toLocaleString()} times)`;
}
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;
if (tests.includes('update')) {
// 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 + slop; iii++) {
const values = getValues(updateProperties[iii % 2]);
const t0 = performance.now();
render(updateContainer, html(updateStrings, ...values));
const t1 = performance.now();
if (iii >= slop) {
updateSum += t1 - t0;
}
if (iii % 1000 === 0) {
await tick();
}
}
const updateAverage = `${(updateSum / updateCount * 1000).toFixed(1).padStart(5)} µs`;
output.textContent += `${output.textContent ? '\n' : ''}update: ${updateAverage} (tested ${updateCount.toLocaleString()} times)`;
}
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);
await run(document.getElementById('default'), XElement, 'inject', 'initial', 'update');
await run(document.getElementById('lit-html'), LitHtmlElement, 'inject', 'initial', 'update');
await run(document.getElementById('uhtml'), UhtmlElement, 'inject', 'initial', 'update');
56 changes: 28 additions & 28 deletions test/test-template-engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -808,8 +808,8 @@ describe('rendering errors', () => {
});

describe('ifDefined', () => {
it('throws if used on a "boolean-attribute"', () => {
const expected = 'The ifDefined update must be used on an attribute, not on a boolean-attribute.';
it('throws if used on a "boolean"', () => {
const expected = 'The ifDefined update must be used on an attribute, not on a boolean attribute.';
const getTemplate = ({ maybe }) => {
return html`<div id="target" ?maybe="${ifDefined(maybe)}"></div>`;
};
Expand Down Expand Up @@ -862,8 +862,8 @@ describe('rendering errors', () => {
container.remove();
});

it('throws if used with "text-content"', () => {
const expected = 'The ifDefined update must be used on an attribute, not on text-content.';
it('throws if used with "text"', () => {
const expected = 'The ifDefined update must be used on an attribute, not on text content.';
const getTemplate = ({ maybe }) => {
return html`<textarea id="target">${ifDefined(maybe)}</textarea>`;
};
Expand All @@ -882,8 +882,8 @@ describe('rendering errors', () => {
});

describe('nullish', () => {
it('throws if used on a "boolean-attribute"', () => {
const expected = 'The nullish update must be used on an attribute, not on a boolean-attribute.';
it('throws if used on a "boolean"', () => {
const expected = 'The nullish update must be used on an attribute, not on a boolean attribute.';
const getTemplate = ({ maybe }) => {
return html`<div id="target" ?maybe="${nullish(maybe)}"></div>`;
};
Expand Down Expand Up @@ -936,8 +936,8 @@ describe('rendering errors', () => {
container.remove();
});

it('throws if used with "text-content"', () => {
const expected = 'The nullish update must be used on an attribute, not on text-content.';
it('throws if used with "text"', () => {
const expected = 'The nullish update must be used on an attribute, not on text content.';
const getTemplate = ({ maybe }) => {
return html`<textarea id="target">${nullish(maybe)}</textarea>`;
};
Expand Down Expand Up @@ -974,8 +974,8 @@ describe('rendering errors', () => {
container.remove();
});

it('throws if used on a "boolean-attribute"', () => {
const expected = 'The live update must be used on a property, not on a boolean-attribute.';
it('throws if used on a "boolean"', () => {
const expected = 'The live update must be used on a property, not on a boolean attribute.';
const getTemplate = ({ maybe }) => {
return html`<div id="target" ?maybe="${live(maybe)}"></div>`;
};
Expand Down Expand Up @@ -1010,8 +1010,8 @@ describe('rendering errors', () => {
container.remove();
});

it('throws if used with "text-content"', () => {
const expected = 'The live update must be used on a property, not on text-content.';
it('throws if used with "text"', () => {
const expected = 'The live update must be used on a property, not on text content.';
const getTemplate = ({ maybe }) => {
return html`<textarea id="target">${live(maybe)}</textarea>`;
};
Expand Down Expand Up @@ -1048,8 +1048,8 @@ describe('rendering errors', () => {
container.remove();
});

it('throws if used on a "boolean-attribute"', () => {
const expected = 'The unsafeHTML update must be used on content, not on a boolean-attribute.';
it('throws if used on a "boolean"', () => {
const expected = 'The unsafeHTML update must be used on content, not on a boolean attribute.';
const getTemplate = ({ maybe }) => {
return html`<div id="target" ?maybe="${unsafeHTML(maybe)}"></div>`;
};
Expand Down Expand Up @@ -1084,8 +1084,8 @@ describe('rendering errors', () => {
container.remove();
});

it('throws if used with "text-content"', () => {
const expected = 'The unsafeHTML update must be used on content, not on text-content.';
it('throws if used with "text"', () => {
const expected = 'The unsafeHTML update must be used on content, not on text content.';
const getTemplate = ({ maybe }) => {
return html`<textarea id="target">${unsafeHTML(maybe)}</textarea>`;
};
Expand Down Expand Up @@ -1142,8 +1142,8 @@ describe('rendering errors', () => {
container.remove();
});

it('throws if used on a "boolean-attribute"', () => {
const expected = 'The unsafeSVG update must be used on content, not on a boolean-attribute.';
it('throws if used on a "boolean"', () => {
const expected = 'The unsafeSVG update must be used on content, not on a boolean attribute.';
const getTemplate = ({ maybe }) => {
return html`<div id="target" ?maybe="${unsafeSVG(maybe)}"></div>`;
};
Expand Down Expand Up @@ -1178,8 +1178,8 @@ describe('rendering errors', () => {
container.remove();
});

it('throws if used with "text-content"', () => {
const expected = 'The unsafeSVG update must be used on content, not on text-content.';
it('throws if used with "text"', () => {
const expected = 'The unsafeSVG update must be used on content, not on text content.';
const getTemplate = ({ maybe }) => {
return html`<textarea id="target">${unsafeSVG(maybe)}</textarea>`;
};
Expand Down Expand Up @@ -1272,8 +1272,8 @@ describe('rendering errors', () => {
container.remove();
});

it('throws if used on a "boolean-attribute"', () => {
const expected = 'The map update must be used on content, not on a boolean-attribute.';
it('throws if used on a "boolean"', () => {
const expected = 'The map update must be used on content, not on a boolean attribute.';
const getTemplate = ({ maybe }) => {
return html`<div id="target" ?maybe="${map(maybe, () => {}, () => {})}"></div>`;
};
Expand Down Expand Up @@ -1308,8 +1308,8 @@ describe('rendering errors', () => {
container.remove();
});

it('throws if used with "text-content"', () => {
const expected = 'The map update must be used on content, not on text-content.';
it('throws if used with "text"', () => {
const expected = 'The map update must be used on content, not on text content.';
const getTemplate = ({ maybe }) => {
return html`<textarea id="target">${map(maybe, () => {}, () => {})}</textarea>`;
};
Expand Down Expand Up @@ -1443,8 +1443,8 @@ describe('rendering errors', () => {
container.remove();
});

it('throws if used on a "boolean-attribute"', () => {
const expected = 'The repeat update must be used on content, not on a boolean-attribute.';
it('throws if used on a "boolean"', () => {
const expected = 'The repeat update must be used on content, not on a boolean attribute.';
const getTemplate = ({ maybe }) => {
return html`<div id="target" ?maybe="${repeat(maybe, () => {})}"></div>`;
};
Expand Down Expand Up @@ -1479,8 +1479,8 @@ describe('rendering errors', () => {
container.remove();
});

it('throws if used with "text-content"', () => {
const expected = 'The repeat update must be used on content, not on text-content.';
it('throws if used with "text"', () => {
const expected = 'The repeat update must be used on content, not on text content.';
const getTemplate = ({ maybe }) => {
return html`<textarea id="target">${repeat(maybe, () => {})}</textarea>`;
};
Expand Down
Loading

0 comments on commit 3c73774

Please sign in to comment.