lwc
lwc copied to clipboard
refactor: Fragment VNode
tl;dr: This PR is a proof-of-concept for fragment VNode. This speeds up child node diffing by removing the need the compare dynamic children in most cases. This new fragment VNode opens up new opportunities and unlocks some capabilities needed for upcoming features.
Details
Context
Currently, the LWC engine uses 2 different diffing algorithms for patching children's vnodes:
updateDynamicChildren- when all the children VNodes are stable across rendering cycles. The children's array has to always have the same length and each individual child has to always have the same position relative to its siblings.- and
updateStaticChildren- other cases.
The updateStaticChildren routine is slower and significantly more complex than its static counterpart as it tries to smartly detect add, removed, and re-ordered children nodes to minimize the amount of DOM operation.
The engine deoptimizes children's diffing:
- if a child node uses either
for:eachoriterator:*directive - if a child node uses the
lwc:dynamicdirective - if a child node is a
<slot>element in a light DOM template - for the slotted and the fallback content of
<slot>elements in light DOM and synthetic shadow DOM - for the iteration content of
for:eachanditerator:*directive
Something interesting to notice here is that dynamic nodes negatively impact their siblings. Let's take the following example to illustrate this:
<template>
<span>Before</span>
<template for:each={list} for:item="item">
<p key={item}>{item}</p>
</template>
<span>After</span>
</template>
In this example, all the top-level elements (the 2 <span> and the N <p>, where N is the number of rendered items) have to go through the dynamic children diffing algo. And this, even if we can guarantee that there is a <span> before and after the N <p>.
Let's take another example where the LWC engine de-optimize children diffing.
<template>
<template for:each={list} for:item="item">
<p key={item}>{item}</p>
<span key={item}>{item}</span>
</template>
</template>
The LWC engine would diff N <p> and N <span> equally, and would have to compare the 2N elements. The LWC engine doesn't take into account the fact that a <p> element is always followed by a <span> element. With this knowledge the LWC engine would only have to compare N stable fragments, each of them composed of a <p> and a <span>.
Proposal
This PR addresses those issues by introducing a VFragment node. This new VNode holds a list of children VNode and anchor nodes indicating the start and end position of the fragment in the DOM. This VNode would allow cleaning up some of the mess we have in the diffing algo. According to the micro-benchmark, we are measuring a 1-3% performance improvement.
for:each and iterator:* improvements:
- Wrap the entire for-block in a fragment to avoid de-optimizing the sibling nodes.
- Wrap each entry in the for-block in a fragment to reduce the amount of diffing between rendering cycles
Light DOM <slot> improvement:
- Replace the
<slot>element with a light DOM with a fragment to avoid de-optimizing the sibling nodes.
if:* improvement:
- When an
if:*directive is applied to a<template>element, wrap the entire content in a fragment to only execute the condition once and not one time per child element.
Forward-looking
A VFragment VNode would certainly help with implementing upcoming slot-related features like scoped slots and dynamic slot names.
Does this pull request introduce a breaking change?
- ✅ No, it does not introduce a breaking change.
Does this pull request introduce an observable change?
- ⚠️ Yes, it does include an observable change.
The new VFragment VNode requires two new empty text nodes which serve anchor points in the DOM. Those empty text nodes are observable from userland via Node.prototype.children. As a side effect, Jest's snapshot tests would fail because those empty text nodes would generate empty lines surrounding the fragment.
Looks amazing!
updateDynamicChildren- when all the children VNodes are stable across rendering cycles [...]- and
updateStaticChildren- other cases.The
updateStaticChildrenroutine is slower and significantly more complex than its static counterpart [...]
I think these two function names are backwards?
Completed via https://github.com/salesforce/lwc/pull/3034