Symbiote VS Lit
The motivation of frontend library and framework developers can vary. If you, as a developer, aim for recognition and industry impact, you need a clear understanding of your purpose. It's challenging to convince users to choose your solution just to save a few kilobytes or milliseconds. Users prefer large communities, rich ecosystems, and established vendors. Your arguments must be compelling. I'll try to show that despite Google's Lit, Symbiote.js is worth considering.
Why Compare with Lit?
Lit is the undisputed leader among libraries based on web components. The fundamental difference of such libraries from all others is that they use native browser component mechanisms (modern DOM API) rather than their own additional runtime. This allows saving user resources and creating more universal solutions. I have been following the development of the Web Components standard group since its inception, and actively participating in its implementation. Lit is a logical continuation of the Polymer project, which started as an adaptation of the draft standard. Since then, a lot has changed. From an enthusiasts' experiment, it has turned into a set of accepted standards, supported by all modern browsers.
Like Lit, Symbiote.js is also a library for creating web components, but with an emphasis on organizing their subsequent interaction.
Traction
Over the past year, the number of weekly downloads of the npm package for Symbiote has increased from 500 to over 5,000. This represents a ten-fold growth in popularity. Considering that Symbiote.js hasn't been actively promoted yet, this is a good result. With the release of the second major version of the library, more effort will be put into promotion.
In comparison, Lit has grown from 700,000 to nearly 1,400,000 downloads. While the scale is incomparable, the growth rate is more gradual, reflecting the overall increase in popularity of web components. However, doubling in popularity at such scales is an excellent result.
Interestingly, the more popular React has shown virtually no growth recently.
License
Symbiote is licensed under the MIT license. Lit is under the BSD-3-Clause license. These licenses are quite similar, but the BSD-3-Clause license additionally restricts the use of author names and brands. I found a good breakdown of the nuances of these two types of licensing here.
Overall, I don't see much difference, and both options are acceptable to me personally.
Size
In terms of size, Symbiote.js wins, but not by much. Symbiote is around 5.13 KB (gzip), compared to around 6.85 KB (gzip) for Lit. Both can be considered compact solutions.
In addition to the size of the library bundle itself, the overall growth in the size of the application with increasing complexity is also important. Comparing by this parameter is more difficult, and I plan to do this in a separate publication, adding React, Vue, and Svelte. There's a surprise waiting for you.
Shadow DOM
I know that working with Shadow DOM can be challenging for many developers. It's a very cool technology, but it can be incompatible with familiar styling practices.
In addition to this, there are at least three approaches to Shadow DOM:
- When the shadow markup exists for all components of the application and styles are added to the shadow root of each
- When shadow parts are created only at certain levels of the DOM hierarchy to guarantee the isolation of these areas (complex widgets, micro-frontends), and all internal parts are styled from the outside, in the general scope
- When Shadow DOM is not used at all, and all classic CSS practices are used (shared CSS on the document)
By default, Symbiote does NOT use Shadow DOM. To enable the shadow mode, an additional flag (renderShadow) or adding styles through the special shadowStyles interface is required. This avoids unnecessary performance overhead and greatly simplifies working with styles in a classic style, with the ability to use custom tag names as convenient selectors.
The rootStyles interface allows you to add styling rules to the top-level shadow root in a hybrid approach. This is an important feature of Symbiote, giving it special flexibility. For example, you can specify isolated local styles and top-level context styles simultaneously.
Lit uses Shadow DOM for all components by default. This behavior can be disabled, but it does not have functions for defining an external top-level shadow root and adding styles to it.
Both Symbiote and Lit can work with the new native browser interface adoptedStyleSheets
, which is part of the CSSOM API.
Templates
In Symbiote, declarative templates are just HTML. This allows you to use HTML files to describe them, use loaders for direct import into your code, or animate a document pre-formed on the server.
In Symbiote.js, like in Lit, there is a tag function (html) for use with template literals, but its result is not a special object (as in lit), but HTML as a string. Thus, in Symbiote.js, the html function is just a helper for more convenient data binding syntax, the use of which is optional.
Also, Symbiote templates are abstract; they are not tied to the context of the component itself, and you can easily write them in separate files or even in separate packages, for example, to use them to generate a document on the server without any additional tools or special "server-side components."
In Lit, to work with server-side rendering (SSR), you will need to use a separate package with its dependencies, which, at the time of writing the article, has an experimental status.
Symbiote.js allows you to define and override (customize) templates in the general document, outside your JavaScript code.
Symbiote's helper function, html, allows you to use regular string interpolation, which does not work with the tag function from the lit-html package used in Lit.
In Lit, rendering templates is tied to the context of the component class, and to make it more abstract, you will have to invent a separate wrapper.
For highlighting the syntax of template literals for Symbiote.js and Lit, you can use the same extension for your IDE; it will work almost identically.
Rendering Dynamic Lists
For rendering lists, Lit uses loops nested in template literals, which in turn are nested in item templates. Alternatively, you can define external loops and insert the ready result into the main template. Lit also provides a special repeat directive designed for efficient list rendering based on keys.
In Symbiote.js, in its itemize API, it uses simple directive attributes itemize and item-tag, which look more compact. And the item template can be simply nested HTML. For efficient updates, you can use updating the source data. Symbiote maximally efficiently makes changes to the DOM, creating new elements and modifying them only when necessary. It can work with item keys and without them, but under the hood, it uses the unkeyed rendering method, which is more performant.
For each list item, Symbiote.js creates its own component instance that implements its own local state, making this solution flexible and efficient. You can also use a pre-defined web component, which can be further optimized. List items, in this case, do not necessarily have to be Symbiote components; they can be any instance of CustomElement
.
The key characteristic in evaluating the efficiency of list rendering approaches is performance. To delve into this topic more deeply, I will also need to write a separate article.
Dependencies
In the Lit package, there are three main dependencies: lit-html, lit-element, and @lit/reactive-element.
Symbiote.js has no external dependencies.
In modern realities, when all current versions of popular browsers support import maps, this point loses some weight. Nevertheless, the simpler the dependency tree of your project, the better, in terms of the possibilities of their management, audit, and the overall reliability of the system.
Additional Features
The basic capabilities of Symbiote and Lit are similar. They both offer a common component model based on the CustomElements standard, declarative templates, reactive data bindings, lifecycle callbacks, and so on.
However, Symbiote has a number of additional features that make it easier to configure interactions between components in their natural environment - the DOM. For example, Symbiote can initialize properties from CSS variable values. This uses a cascading model of configuration propagation, which can be bundled with other customizations for your UI entities. You can easily define and override component properties for entire branches of the DOM tree, as easily as defining their styling rules. And you won't need any additional libraries or functions for this; everything works on the standard DOM API and CSS.
In addition, Symbiote components can directly connect to data contexts of top-level components in the hierarchy and create shared contexts (this works similar to the behavior of built-in radio inputs, which "know" the state of their colleagues by the name attribute). Thus, you can implement a complex graph of relationships between components without the need for any additional tools.
Examples
Here are two examples of component code that are functionally identical. The original example is taken from the official Lit website:
import { LitElement, html, css } from 'lit';
export class MyElement extends LitElement {
static properties = {
greeting: {},
planet: {},
};
static styles = css`
:host {
display: inline-block;
padding: 10px;
background: lightgray;
}
.planet {
color: var(--planet-color, blue);
}
`;
constructor() {
super();
this.greeting = 'Hello';
this.planet = 'World';
}
render() {
return html`
<span
@click=${this.togglePlanet}>${this.greeting}
<span class="planet">${this.planet}</span>
</span>
`;
}
togglePlanet() {
this.planet = this.planet === 'World' ? 'Mars' : 'World';
}
}
customElements.define('my-element', MyElement);
The same example written using Symbiote.js:
import { Symbiote, html, css } from '@symbiotejs/symbiote';
export class MyElement extends Symbiote {
init$ = {
greeting: 'Hello',
planet: 'World',
togglePlanet: () => {
this.$.planet = this.$.planet === 'World' ? 'Mars' : 'World';
},
};
}
MyElement.shadowStyles = css`
:host {
display: inline-block;
padding: 10px;
background: lightgray;
}
.planet {
color: var(--planet-color, blue);
}
`;
MyElement.template = html`
<span
${{onclick: 'togglePlanet'}}>{{greeting}}
<span class="planet">{{planet}}</span>
</span>
`;
MyElement.reg('my-element');
As you can see, the second example is about ten lines shorter. With Symbiote, boilerplate code is generally reduced in the vast majority of cases.
Conclusion
The conclusions are up to you. As the author of this article, I am myself a skeptic and not a fan of pointless entropy increase. All the differences and nuances described above have arisen from real-world problem-solving practices, often quite non-trivial. It's not certain that you will personally encounter such problems, but if you do, Symbiote won't let you down.