Skip to content

Instantly share code, notes, and snippets.

@WebReflection
Last active February 6, 2024 15:50
Show Gist options
  • Star 88 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save WebReflection/761052d6dae7c8207d2fcba7cdede295 to your computer and use it in GitHub Desktop.
Save WebReflection/761052d6dae7c8207d2fcba7cdede295 to your computer and use it in GitHub Desktop.
A recap of my FE / DOM related libraries

My FE/DOM Libraries

a gist to recap the current status, also available as library picker!

Minimalistic Libraries

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?)

Reactive Primitives (client/server)

  • µsignal (a tiny blend of solid-js and preact/signals)
  • µhooks (a React hooks alternative)

Enriched Libraries

compromise between small and features rich

Combo Libraries

integrated libraries

Minimalistic Combo

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

DOM Engines Features Comparison

µ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

About HTML injection safety

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.

Companions & wrappers

  1. vanilla JS
  2. µce / µce-template
  3. µhtml-ssr / µcontent
  4. wickedElements
  5. hookedElements
  6. neverland
  7. heresy
  8. heresy-ssr
  9. HyperHTMLElement
  10. µland

DOM Engines Summary

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

hyperHTML Pros and Cons

  • 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

lighterhtml Pros and Cons

  • 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

µhtml Pros and Cons

  • 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
- - -

F.A.Q.

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.

@WebReflection
Copy link
Author

WebReflection commented Feb 26, 2020

@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 👋

@ryansolid
Copy link

Historically this would have been a non-starter for me, but modern UI patterns have softened this more. I was really into Web Components in the past as a substitute for a "Component" system. Which meant many layers of nesting and ultimately local state being held in the DOM. So nodes were reusable in the sense that when attributes and properties change they could derive new data, but being keyed by index in any sort of list meant that any Component that needed to do some more expensive operation like an AJAX request would redo it. Obviously that is pretty dumb to do 1 per in a list of a hundred items, but it doesn't take much nesting to naturally produce these sort of scenarios. Maybe it was some sort of view calculation or specific data derivation, maybe an integration with an older jQuery or Bootstrap plugin. Ultimately unlike say VDOM Components where the state is kept in a virtual representation that needed to be remapped anyway, the state was stored in these DOM nodes and using index which effectively change the prop/attributes meant blowing away and redoing most of the downstream work anyway.

Obviously nowadays I make my Web Components lighter and use them generally with existing library/frameworks, and have better patterns around hoisting data and global stores so while I still just naturally default to keyed in all but the most trivial cases, I could see it not making a difference anymore. But I think libraries that intend to work with Web Components or promote the concept their library just outputs real DOM nodes should be much more conscious of this than any other library. It can lead to wrong expectations if people aren't careful as they might just interact with those DOM nodes directly creating side effects. And it's important to consider once you introduce key by index in a tree, any nested keyed approaches often are negated and you get the worst of both worlds. You almost have to do it all one way or the other, otherwise, you risk the highest cost as keyed operations are more expensive and if key by index is any ancestor the child keys will like never match.

@WebReflection
Copy link
Author

@ryansolid this is gold, and thanks for sharing it, it's the first time I realize custom elements in a non keyed render can indeed be nasty, especially if the custom elements initializes itself on the constructor, or it doesn't observe the keyed reference (i.e. data-id) or any accessor (i.e. .key=${value})!

And yet, if the custom element needs to do, or show, something completely different, accordingly with its keyed value, the non-keyed approach can easily be a disaster prone one.

If worth nothing, my thinking behind µhtml is that the Web is not really into Custom Elements, and the most obvious/common pattern is to store relevant info on any node, and call it a day, and that worked for me too.

However, µce aim is to bring custom elements everywhere, through µhtml, with the minimal amount of shipped code, but that's also the original goal of wickedElements, or even hookedElements, as these two don't even need Custom Elements to be available at all, and yet these would suffer the non-keyed result issue you've described already, in case nodes are shifted around with no keys.

As summary, I think you kinda convinced me to include keyed renders in µhtml, for the simple reason that it'd be otherwise just am entry point to instantly switch to lighterhtml, and as such, it'd be less useful.

Maybe this is the only, must have, minimalistic feature I should bring in, to call the µhtml features complete, so while I'm thinking about the best way t do that, I want to thank you one more time for these concerns: people don't realize how important these are when it comes to custom elements 👍

@WebReflection
Copy link
Author

@ryansolid
Copy link

@WebReflection that is great. I'm so glad this gets in there. I think there are places where non-keyed benefits are worthwhile but keyed approach is essential.

Aside: I never understood why this feature was not more common in many might as well be made for Web Component libraries. VDOM community went through the same cycle 2014-15ish and landed on keyed as the default for the most part. I think it is even more important here.

But I get that because of being essentially a single pass system you need a special helper method to do keyed instead of just using the language so to speak. This is one of the only things of this nature with this approach so it's a harder sell. Whereas reactive libraries have the same constraint (also single pass on render) but we need many more of those so adding one for mapping isn't that big of a deal.

@WebReflection
Copy link
Author

WebReflection commented Feb 27, 2020

@ryansolid it landed, and forced me to create udomdiff, otherwise js-framework-benchmark would've failed big time on keyed.

Well, it ended up even faster than lighterhtml, but the size now is 2.8, instead of 2.5 ... I guess we can live with that.

uhtml-lighterhtml

Keyed Performance

A comparison with uhtml, lighterhtml, hyperHTML, and lit-html

uhtml-lighterhtml-hyperhtml-lit-html

@ryansolid
Copy link

Very nice. It looks better across the board especially with reductions in size and memory usage. But definitely nice performance boost, that should move it into the top 15 (skipping reference vanilla implementations) making it the uncontested fastest non-reactive tagged template literal implementation in the benchmark.

@msand
Copy link

msand commented Feb 29, 2020

Thanks for the summary @WebReflection fascinating to follow your work. I'm wondering, what it would take for me to integrate react-native-svg, or make it compatible with nativeHTML, perhaps that could increase interest in it? Been thinking about making it compatible with flutter as well, and normal native mobile development. Was intending to look into nativeHTML some time ago, but work and other priorities haven't made that feasible yet. Would you be able to compare it to the architecture of react-native / ViewManagers? Perhaps it can help me make a proof of concept faster.

@WebReflection
Copy link
Author

@msand as pity as it sounds, I can't, and don't want to, compete with React native. It's like ... one person against Spartans.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment