How lit-html works
Unlike most modern front-end JavaScript libraries, lit-html does not use a virtual DOM. Instead, it utilizes tagged template literals, like this:
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
receives an array of strings and values, such as the expression name
in the example above.
TemplateResult
serves as a container to obtain a TemplateInstance
from a cache and is also responsible for creating a HTMLTemplateElement
by joining all the static strings with a special comment node.
By removing this abstraction, the code above can be simplified 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 new object. The render
function takes this new object and 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.
During the first render, a NodePart
is created and stored in a WeakMap, using the node as the key. This ensures that if the node is removed from the DOM, the NodePart
instance will also be garbage collected, preventing memory leaks.
If we remove the abstraction of render
, we get 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 its construction, it traverses the template element and replaces special comment nodes with empty comment nodes. This indicates where values will be injected in the future.
NodePart
NodePart
inherits from the parent class Part
. It is responsible for using native JavaScript methods to render the 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, its _clone
method will be invoked. However, the name is misleading, as it appears to create a native browser DocumentFragment
instance by traversing the template and generating the necessary nested NodeParts
. The newly created DocumentFragment
instance is then appended to the node.
Updating a TemplateInstance
with new values causes all the sub NodePart
s to update 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 the 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 performs minimal work by keeping track of “holes” where new values can occur and replacing 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.