a gist to recap the current status, also available as library picker!
do one thing only and do it well
- µhtml (HTML/SVG auto-keyed and manual keyed render)
- augmentor (hooks for anything)
- wickedElements (custom elements without custom elements ... wait, what?)
compromise between small and features rich
- µce Custom Elements via µhtml (see a comparison w/ lit-element)
- µce-template a toolless Vue 3 like approach
- lighterhtml (HTML/SVG auto-keyed and manual keyed render)
- hyperHTML (HTML/SVG with no auto-keyed but optional manually keyed render)
- HyperHTMLElement (custom elements via hyperHTML)
- dom-augmentor (augmentor with DOM auto-hooked useEffect)
integrated libraries
- hookedElements (wickedElements + augmentor)
- neverland (lighterhtml + dom-augmentor)
- µland (µhtml + dom-augmentor)
- heresy and heresy-ssr (lighterhtml + augmentor + Custom Elements)
you choose what to use
- wickedElements & µhtml or lighterhtml (easy)
- hookedElements & µhtml or lighterhtml (even easier)
- dom-augmentor & µhtml or lighterhtml (not easy at all, try µland or neverland instead)
- native Custom Elements and µhtml or lighterhtml are an option too
µhtml | lighterhtml | hyperHTML | |
---|---|---|---|
released | early 2020 | late 2018 | early 2017 |
browsers compatibility | IE11+ & mobile | IE9+ & mobile | IE9+ & mobile |
brotli min & transpiled | 2.8K | 4.8K | 6.8K |
brotli min & ecmascript | 2.5K | 4.2K | not built |
performance | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
simple/friendly API | ✔ | ✔ | |
sparse attributes | ✔ | ✔ | |
events attribute | ✔ | ✔ | ✔ |
events options | ✔ | ✔ | |
style special case |
opt-in: µstyler | ✔ | ✔ |
aria special case |
✔ | since v3 | |
ref special case |
✔ | ✔ | |
.dataset=${obj} helper |
✔ | ✔ | |
data & props setters |
✔ | ||
direct .setters=${...} |
✔ | ✔ | since v2.32 |
boolean ?attr=${...} |
✔ | since v4.2 | since v2.34 |
has own components too | ✔ | ||
user-land defined intents | ✔ | ||
auto keyed nodes | ✔ | ✔ | |
manually keyed nodes | ✔ | ✔ | ✔ |
Custom Elements compat. | ✔ | ✔ | ✔ |
Shadow DOM compat. | ✔ | ✔ | ✔ |
NodeJS SSR compat. | µhtml-ssr / µcontent | heresy-ssr | viperHTML |
hooks friendly | ✔ | ✔ | ✔ |
<self-closing-tags /> |
✔ | ✔ | ✔ |
attributes callbacks | ref only |
✔ | ✔ |
content callbacks | ✔ | ✔ | |
promises as content | ✔ | ||
promises placeholders | ✔ | ||
dis/connected events | only version plus | ✔ | |
automatic fragments | ✔ | ✔ | ✔ |
interpolated HTML content | ✔ | ✔ | ✔ |
HTML injection safety | ✔ | ✔ | ⚠ |
Companions & wrappers | 1, 2, 3, 4, 5, 11 | 1, 4, 5, 6, 7, 8 | 1, 9 |
Features Score | 30 | 33.5 | 26.5 |
In hyperHTML an array of strings, or primitives, used as interpolation is automatically injected as HTML. This was an early design/feature mistake, non existent in other libraries.
Both lighterhtml and hyperHTML accepts an explicit {html: ...}
interpolation when injecting HTML is meant, hence not accidental, while uhtml can either use ref=callback(node)
or use html([txt])
instead of a template literal to parse [txt]
as if it was a template literal, but it's parsed each time if the array is different each time, hence discouraged, yet possible.
- vanilla JS
- µce / µce-template
- µhtml-ssr / µcontent
- wickedElements
- hookedElements
- neverland
- heresy
- heresy-ssr
- HyperHTMLElement
- µland
In older to newer library order
- hyperHTML: early 2017, first of a kind, early design, battle tested
- lighterhtml: late 2018, much better DX and easier to integrate than hyperHTML
- µhtml: early 2020, it's an essential subset of lighterhtml
- shipped to millions since 2018, enterprise grade
- today is best used via its HyperHTMLElement helper
- it included too many features that are rarely used
- it's features complete, mostly maintenance only
- it has a viperHTML SSR counter part, but it's been deprecated
- smaller than hyperHTML but even if it shares most of its core, it's faster in most cases
- it has a superior DX compared to hyperHTML
- almost every internal behavior can be customized
- it plays better with hooks and
ref=${...}
pattern
- it's an essential lighterhtml subset, for half of the size
- you can start small, and switch to lighterhtml without touching code
- the only cons is that is a subset with its own constrains and/or limited features
Isn't all this hard to maintain?
Pretty much all my libraries share the same code behind the scene. The difference is in the user-facing API and the pattern provided by such API (hooks vs Custom Elements vs just render, etc).
If you find a bug in heresy, as example, and it comes from augmentor, everything else hooks based will be patched automatically.
Same goes for lighterhtml and hyperHTML, sharing domdiff module and much more: if I fix something there it'll propagates everywhere else.
uhtml is a little exception to the latter case but ... well, it's tiny indeed, so it's the easiest to patch/maintain, specially 'cause it's code is mostly a copy/paste from lighterhtml, so that if I fix one, the other would pseudo-automatically follow.
What does auto-keyed mean?
All render engines avoid DOM trashes as much as they can. There are basically two ways to do this: use an index, automatically recycling the same node with such index, or use an explicit reference, manually coupling a specific part of the stack to such reference.
µhtml and lighterhtml support both approaches: in the former case, each node keeps being updated with new info, if found at the same position it was before, and only if the the new info changed, keeping data and UI strictly decoupled.
On the other hand, all rendering engines allows explicit references: same reference gets same node/stack each time, and DOM nodes are moved around whenever the reference is found in another position (i.e. list of items).
In the µhtml and lighterhtml case, the index, auto-keyed, approach, is the default, but you can always use html.for(...)
or svg.for(...)
to switch to the keyed pattern, following an example:
// auto-keyed lists example
render(viewNode, html`
<ul>
${items.map(item => html`<li>${item.text}</li>`)}
</ul>
`);
// manually keyed lists example
render(viewNode, html`
<ul>
${items.map(
item => html.for(item)`<li>${item.text}</li>`
)}
</ul>
`);
However, if the list of items have a unique id, you can always use <li data-id=${item-id}>
in case you need to retrieve the associated data later on.
@ryansolid if I might ask, how important is for you the keyed case? beside the fact that one could just use lighterhtml when/if needed, do you have precise use cases for that? I haven't found many in my experience, and coupling
data-id=${item.id}
seems like a reasonable solution, when exact item is needed, keeping data and UI decoupled. What do you think?P.S. I have added an entry in the F.A.Q. section, so thanks for asking this, it clearly needed some clarification 👋