Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement DOM reconciliation for templates #82

Merged
merged 1 commit into from
Aug 13, 2024

Conversation

ehellman
Copy link
Collaborator

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 the cpu provider, and depending on the cpu.usage value we add different class names to the icon.:

<!-- Current DOM -->
<div class="template" id="cpu">
  <i class="cpu-icon"></i>
  <span class="cpu-value">24%</span>
</div>

<!-- Desired DOM -->
<div class="template" id="cpu">
  <i class="cpu-icon high"></i>
  <span class="cpu-value">69%</span>
  <i class="cpu-warn></i>
</div>

If you imagine changes to the DOM like commits in git, the current change commit would look like this:

- <div class="template" id="cpu">
-   <i class="cpu-icon"></i>
-   <span class="cpu-value">24%</span>
- </div>
+ <div class="template" id="cpu">
+   <i class="cpu-icon high"></i>
+   <span class="cpu-value">69%</span>
+   <i class="cpu-warn></i>
+ </div>

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:

zebardomdiff_before

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:

<!-- Current DOM -->
<div class="template" id="cpu">
  <i class="cpu-icon"></i>
  <span class="cpu-value">24%</span>
</div>

<!-- Desired DOM -->
<div class="template" id="cpu">
  <i class="cpu-icon high"></i>
  <span class="cpu-value">69%</span>
  <i class="cpu-warn></i>
</div>

Would mean that these changes are required:

  • The node for: i.cpu-icon should do node.classList.add('high')
  • The node for span.cpu-value should do node.textValue = newValue
  • The i.cpu-warn node is added at the end of the children of div#cpu.template, so a node.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 as morphdom 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, while snabbdom took several milliseconds. The VirtualDOM overhead seems unnecessary for now, and the performance is better so I went with morphdom. 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?

<div class="template" id="cpu">
-   <i class="cpu-icon"></i>
+   <i class="cpu-icon high"></i>
-   <span class="cpu-value">24%</span>
+   <span class="cpu-value">69%</span>
+   <i class="cpu-warn></i>
</div>

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:

zebardomdiff_after

@ehellman ehellman force-pushed the feat/template-dom-reconciliation branch from b6c60e9 to 4c7a130 Compare August 13, 2024 18:07
@lars-berger lars-berger merged commit 480613f into glzr-io:main Aug 13, 2024
5 checks passed
Copy link

🎉 This PR is included in version 2.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: ✅ Done
Development

Successfully merging this pull request may close these issues.

2 participants