feat: Implement DOM reconciliation for templates #82
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Implement DOM reconciliation for templates
The problem
Today, there's an issue with the logic around Zebar templates and how they are updated in the DOM. The current implementation replaces the
innerHTML
of the template node with the updated HTML from the templating engine.This causes several issues, the most notable one being that it messes with CSS animations, which with the current implementation cannot have an "out"-animation for stuff like hover or classname toggling since the entire element is removed and re-inserted into the DOM - the "in" animation happens instead.
The solution to this, as well as a more efficient way to solve re-rendering of templates is DOM reconciliation and patching, meaning that only the operations that are needed for us to go from the original DOM tree to the desired DOM tree are performed.
Basic example where we in our template prints out the
cpu.usage
value from thecpu
provider, and depending on thecpu.usage
value we add different class names to the icon.:If you imagine changes to the DOM like commits in git, the current change commit would look like this:
We can see this if we observe the DOM in the devtools, the flashing nodes are the ones that are updated and we can see that even the nodes that don't have an update on them flash, meaning that everything is removed and re-inserted:
The optimal scenario would be to compare the current DOM tree with the desired DOM tree with an algorithm that can calculate the minimum amount of operations possible (without just replacing everything) required on the old DOM to achieve the state of the desired DOM. Which, in our earlier scenario:
Would mean that these changes are required:
i.cpu-icon
should donode.classList.add('high')
span.cpu-value
should donode.textValue = newValue
i.cpu-warn
node is added at the end of the children ofdiv#cpu.template
, so anode.appendChild(newNode)
is required.The solution
This PR utilizes a package called
morphdom
that compares the trees of 2 nodes and calculates the correct operations needed and performs them recursively.I also tried to use
snabbdom
which basically converts your DOM nodes to VirtualDOM first, does the calculations and then patches the DOM in the same manner asmorphdom
does. However, when I compared the time it took to perform updates,morphdom
was A LOT faster. With regular template sizes the updates happen in 0.1-0.4ms, whilesnabbdom
took several milliseconds. The VirtualDOM overhead seems unnecessary for now, and the performance is better so I went withmorphdom
. It's worth noting that the actual end result of both of them are the same.So what does the same DOM update look like in this PR?
Sadly, markdown doesn't support just showing the parts of the line that is diffing from the removed line. But we can see that the outer div remains unchanged while the other values have updated as expected. It's much easier to spot the difference in the devtools: