How lit-html works
Unlike most modern front end JavaScript libraries, lit-html does not use a virtual dom. Instead, it uses tagged template literals, as such:
import { html, render } from 'lit-html';
// A lit-html template uses the `html` template tag:
const sayHello = (name) => html`<h1>Hello ${name}</h1>`;
render(sayHello('World'), document.body);
The tagged template literal html
passes its arguments to a TemplateResult
class. This means that TemplateResult
is getting an array of strings and values which would be expressions like name
in the example above.
TemplateResult
is both a container type to obtain a TemplateInstance
from a cache and also in charge of creating a HTMLTemplateElement
by joining all the static strings with a special comment node.
By removing this abstraction, the above code is roughly de-sugared to something like this:
import { render } from 'lit-html';
const templateStringsArray = ['<h1>Hello ', '</h1>'];
const sayHello = (name) => {
const templateElement = document.createElement('template');
templateElement.innerHTML = '<h1>Hello <!--{{lit-5060303367651138}}--></h1>';
return {
templateStringsArray,
values: [name],
templateElement,
};
};
render(sayHello('World'), document.body);
When we call sayHello
with a new name, it creates a brand new object. The render
function takes in this new object, along with the node to render into, which is document.body
in this case.
If the node has already been rendered, it simply sets the new object and commits.
On the first render though, a NodePart
will be created and stored in a WeakMap with the node as the key. This ensures that if the node is removed from the dom, the NodePart
instance will be garbage collected as well, preventing any memory leak.
If we remove the abstraction of render
, we end up with this:
import { parts, NodePart } from 'lit-html';
const templateStringsArray = ['<h1>Hello ', '</h1>'];
const sayHello = (name) => {
const templateElement = document.createElement('template');
templateElement.innerHTML = '<h1>Hello <!--{{lit-5060303367651138}}--></h1>';
return {
templateStringsArray,
values: [name],
templateElement,
};
};
const node = document.body;
let part = parts.get(node);
if (part === undefined) {
node.innerHTML = ''; // empty node
part = new NodePart();
parts.set(node, part);
}
const templateResult = sayHello('World');
part.setValue(templateResult);
part.commit();
Let's dig into what NodePart
does.
Template
Template
is a container that keeps track of values' indices and the template element.
During construction, it traverses the template element and replaces the special comment nodes with empty comment nodes to indicate where to inject values in the future.
NodePart
NodePart
inherits from the parent class Part
. It is in charge of native JavaScript methods to render a dom.
Its setValue
method simply stores the new value as a __pendingValue
. The heavy lifting is done in the commit
method, which will call various helpers based on their type, in this case, __commitTemplate
.
__commitTemplate
will either create or update the internal instance of the TemplateInstance
class. Deconstruction of the above methods results in the following code:
import { parts, TemplateInstance } from 'lit-html';
const templateStringsArray = ['<h1>Hello ', '</h1>'];
const sayHello = (name) => {
const templateElement = document.createElement('template');
templateElement.innerHTML = '<h1>Hello <!--{{lit-5060303367651138}}--></h1>';
return {
templateStringsArray,
values: [name],
templateElement,
};
};
const node = document.body;
let part = parts.get(node);
if (part === undefined) {
node.innerHTML = '';
part = {};
parts.set(node, part);
}
const template = sayHello('World');
if (part.templateInstance === undefined) {
part.templateInstance = new TemplateInstance(template);
const fragment = part.templateInstance._clone();
node.appendChild(fragment);
}
part.templateInstance.update(template.values);
If a TemplateInstance
is created, it's _clone
method will be called. However, it is badly named, as it seems to create a browser native DocumentFragment
instance instead by walking the template and creating necessary nested NodeParts
. The newly created DocumentFragment
instance is then appended into the node.
Updating a TemplateInstance
with new values results in all the sub NodePart
s updating as well, replacing their original dom node markers in the document. This is very efficient as only the nodes that are changed by values are replaced. The static nodes are not touched.
This is a simplification of the template algorithm after we remove main NodePart
and TemplateInstance
:
import { parts, TemplateInstance } from 'lit-html';
const templateStringsArray = ['<h1>Hello ', '</h1>'];
const sayHello = (name) => {
const templateElement = document.createElement('template');
templateElement.innerHTML = '<h1>Hello <!--{{lit-5060303367651138}}--></h1>';
return {
templateStringsArray,
values: [name],
templateElement,
};
};
const node = document.body;
let part = parts.get(node);
if (part === undefined) {
node.innerHTML = '';
part = {};
parts.set(node, part);
}
const template = sayHello('World');
if (part.templateInstance === undefined) {
part.templateInstance = {
template,
subparts: [],
};
const fragment = document.importNode(template.templateElement.content, true);
node.appendChild(fragment); // mount the entire fragment
const subPart = fragment.firstChild.childNodes[1]; // reference to the comment node
subpart.insertAdjacentElement('beforebegin', template.values[0]);
part.templateInstance.subparts = [subPart];
}
// replace all the parts with new values
for (let i = 0; i < template.values.length; i++) {
const subpart = part.templateInstance.subparts[i];
subpart.previousSibling.replaceWith(template.values[i]);
}
Conclusion
In essence, lit-html is doing minimal work by keeping track of "holes" where new values could occur and replace them. This has the benefit of doing less JavaScript work at the expense of more inefficient dom operations.
Of course, in this investigation, we also skipped over various things such as updating of attributes, event listeners, efficient list updating, and security aspects. I highly encourage reading lit-html's code to get a better understanding of its mechanisms. Although it is a little confusing due to the layers of indirection, there are plenty of comments that explain higher-level concerns and operations.