I made a better DOM morphing algorithm
At least I think it’s better, but also I could also be missing something obvious.
74 points by joeldrapper - 38 commentsAt least I think it’s better, but also I could also be missing something obvious.
74 points by joeldrapper - 38 comments
Most websites work fine with plain html. If you need something fancier, the world seems to have settled on using React.
I get that this is to let you render html on the backend and then stream it to the site so that JS can update the dom. But why? Genuine question; I’m not saying there’s no good reason.
Rendering the whole DOM tree (instead of VDOMs) is a fast process. The slow part is attaching (committing) elements to the doc. e.g., I have a test of 20,000 elements which takes <30ms to render, while attaching them takes 120ms.
Since the performance is mainly bound to the commit phase, libraries like these (and hopefuly a native API) help for creating simple UI frameworks. For example, a helper such as:
could be used, like: Here's an example of using that helper:https://github.com/ericfortis/mockaton/blob/main/src/client/...
The world might have, but I personally have not!!! x(
(I don't think the world really has, the same way the world moved on from jQuery at some point :) and jQuery was probably more widespread)
If anything it removes a ton of the argument for using React absent maybe a small subset of highly complex UI subcomponents which may need it, but rarely required for a whole SaaS app. Frontend teams just want React so they can use a single tool not because it's the best solution.
Full page reloads are fine for most CRUD cases. Then layering DOM morphing can be even better UX almost for free
*Edit fixed typo.
The way I do it is to update everything _except_ for the DOM nodes that need to be excluded (via data attributes), e.g. the sidebar or a video player. I have found no problems with this approach as I maintain state since the JS is already running before clicking a link, and everything else is updated.
I think this is for if you absolutely have to SSR your markup at all times (e.g. using HTMX), but with something like Alpine.js and using <template> elements, there is no reason to DOM morph.
And like you say, if you need to use crazy advanced DOM morphing, you should probably be using a client side framework anyway. If not, I've gotten away with some very tricky state updates with just Alpine.js.
I've been working on some prototypes for possible immutable DOM APIs and though I haven't gotten that far in experimenting with it I've been expecting to encounter the same problem that this library is designed to solve: in my system the DOM will be represented as a deeply immutable tree of JS objects, arrays, and such, so a state update might consist of being given references to two immutable trees, the current state and the desired state, and from there you need to compute a minimal set of changes so that you don't redo layout and drawing work that doesn't need to be redone for parts of the DOM that are unchanged. This sounds like exactly the algorithm you'd want to do that! So basically it could allow me to use the immutable DOM representation as the source of truth and then diff and sync the new state onto the mutable DOM
https://github.com/geon/react-node-diff/blob/main/src/diff-r...
I looked it up on Github and they seem to be using the idiomorph package.
[1] https://dev.37signals.com/turbo-8-released/
https://dm.rt.ht/perf
Morphlex is 8→69× faster and 4× smaller!
To see `moveBefore()` in action:
try moving the clock.gif line up/down with Alt+↑ and Alt+↓!
Does your library support the new state-preserving moveBefore method?
I will test your library extensively and update you via GitHub in case of questions/issues!
Thank you for releasing Morphlex!
1. Coming from a client-side rendering perspective, DOM morphing/diffing is 99% of the time a bad idea, except in the case of reordering a list of keyed items where you can use a simpler, more specialized algorithm.
It's much better to use template identity to compare the source template of the current DOM with the source template of the incoming DOM (or description of DOM) and completely re-render if the source template changed. It's a very simple and fast check, and nearly all the time you change templates you want new DOM state anyway.
This technique works with SSR'ed HTML as well. You leave marker comments in the HTML that bracket the nodes created from a template and carry with them a template ID (e.g. a hash of the template). When updating the DOM, as you traverse the template instance tree, you check IDs and replace or update in-place as needed. Again, simple and fast.
2. But... If you're morphing the existing DOM, this seems to eliminate many of the benefits of sending HTML in your server responses in the first place. The HTML is just data at that point - you parse it only to crawl it as a set of instructions for updating the DOM.
HTML is an expensive way to do this. It's a larger format and slower to parse than JSON, and then you have to do this diffing. You'd be better off doing client-side rendering if possible. Data + templates is usually a very compressed format compared to the already expanded HTML you get from rendering the templates client-side.
And if the reason to morph is to keep things like event listeners, templates would let you attach those to the new DOM as well as preserve them in the unchanged DOM. With DOM morphing you need a way to go set things up on the new DOM anyway.
...
The big advantage of this is the architectural simplicity of only ever returning HTML from the server, as opposed to HTML for first render and data for updates, but it's not going to have good network and rendering perf compared to CSR for updates.
This approach works for simple stuff, until it doesn't. Form inputs will lose values, focus will be lost (ruining accessibility in the process), videos will restart, etc. You need the diffing to prevent unnecessary changes to the DOM. Even worse, in complex applications you easily end up in situations where the trivial approach causes vast swathes of the page to rerender at once, either because of unplanned dependencies or simply because you have three, seven or forty teams working on the site at the same time.
From experience, this works in apps like photo editors, video platforms, forums, app stores, home automation, application builders... It's just extremely rare to have two totally different templates with a shared element in them that you want to keep stable - the literally 99.9% case is that if the template identity changes, the DOM should be cleared.
With DOM state being thrown away, it would still not be possible to build as-you-type input validation for example. For SSR + streaming server updates I get the feeling it would also have limited utility, how do you track dependencies across more complex template conditions, loops? Is querying for comment markers any faster than traversing DOM elements? If using generated IDs, do you keep node IDs in memory for each user session in the server when dealing with dynamic content? Are you using an existing open source solution for this?
The DOM diffing/morphing approach is popular because it's in fact extremely fast to run, has small memory requirements, and is a low complexity implementation. In the SSR case, you don't need anything special on the server side, it can be completely ignorant of what is happening in the client. It's hard to beat.
The event listener part is easy use a top level event handler and bubble up events.
The network part is also easy brotli compression over an SSE stream. Even though this demo returns around 180kb per frame uncompressed, a single check between frames will compress to 13bytes on the wire.
https://checkboxes.andersmurphy.com/
See https://news.ycombinator.com/item?id=45941553
The playground you asked for has already helped me file and fix a bug https://github.com/yippee-fun/morphlex/issues/38 in my brief review of Morphlex.
See https://dm.rt.ht
Just updated to demonstrate what `moveBefore()` brings to the table!!
Try moving the clock.gif line up/down with Alt+↑ and Alt+↓!
(it is very nascent, and I am on a plane with shaky Wi-Fi, so I will give it the love it deserves in a couple of days: compare all libraries with each other, add a benchmark, etc.)