Jovi De Croock

Software Engineer

Fragments in Virtual DOM

One thing that I've been amazed by while working on Preact is the complexity introduced by something seemingly simple, the Fragment.

For people unfamiliar with the Fragment in Preact/React, it's a Virtual DOM-structure that allows a component to return multiple children.

Not to be confused with the GraphQL Fragment

If you'd have a component that does the following

const App = () => (
  <span>Hello</span>
  <span>World</span>
)

We'd complain and tell you that it would need to be wrapped with a Fragment which essentially comes down to wrapping it with Fragment or \<\>.

Now if we look at how a Fragment is implemented in Preact it's just a component that returns its children.

export const Fragment = props => props.children

Any component that just returns its children can be considered a Fragment.

What makes this so complex? It creates an additional distinction between the DOM and our virtual equivalent. When we would initially mount a component that looks like the aforementioned App we'd do the following operations:

  • Call <App /> so we get the resulting VNodes
  • We traverse them, first item we encounter is a Fragment
  • We render the Fragment just to see it return its children
  • We perform appendChild() twice for the <span>
  • We populate each span with a text-node
VDOM:
<App>
  <Fragment>
    <span>hello</span>
    <span>world</span>

DOM:
<span>hello</span>
<span>world</span>

That wasn't complex at all, and I am completely with you. The complexity starts when we are unmounting and guaranteeing the correct ordering, when we are mounting new nodes. Let's expand this sample a bit:

const Todos = () => (
  <Fragment>
    <h1>Todos</h1>
    <ul></ul>
    <button></button>
  </Fragment>
)

const Persons = () => (
  <Fragment>
    <h1>People</h1>
    <ul></ul>
    <button></button>
  </Fragment>
)

const App = () => (
  <main>
    <Persons />
    <Todos />
  </main>
)

Which would result in

VDOM:
<App>
  <main>
    <Fragment>
      <h1>People</h1>
      <ul></ul>
      <button></button>
    <Fragment>
      <h1>Todos</h1>
      <ul></ul>
      <button></button>

DOM:
<main>
  <h1>People</h1>
  <ul></ul>
  <button></button>
  <h1>Todos</h1>
  <ul></ul>
  <button></button>
</main>

When we'd switch <Todos /> to live in front of <Persons /> we'd have to move all three of its Dom-nodes in front of the first DOM-node wrapped with this Fragment so we'd have to perform insertBefore(x, <h1>People</h1>) three times. This however means that we have to store the firstDomChild of this Fragment to effectively perform this operation.

That still sounds quite... convenient, doesn't it? Well this nesting could be any number, think about the amount of Context wrappers or components that just fetch data until you reach your first DOM-node? In most applications this isn't trivial, and there are many more operations we have to consider, this is mainly a moveBefore while there are many more operations to consider here.

The move from <Todos /> before <Persons /> could have been accompanied by a different state change that results in us returning null from <Todos /> which means we'd have to unmount all of that DOM so the most efficient course of action here is to not perform the three insertions at all and just unmount all of the DOM-nodes.

Linearly if we're moving something between two components we have to discover where the first component ends and the next begins, this means that we'd need to look through their existing children and find out which is the last DOM-child, however, as we have discussed earlier some of those children could be null, some could be fragments and some could be DOM-nodes. Exploring these deeply for each and every operation would be quite expensive, luckily for a lot of these operation we can keep our firstDomChild heuristic where we'd look at the first DOM-node of the component that will come after the node we are inserting and perform an insertBefore.

Now you know why we store _nextDom and have namings like firstChildDom in our diffChildren algorithm.

All of these are small insights into what Preact, React, Vue and many others are abstracting away from you, the declarative way of writing web-applications comes with a cost and a lot of hidden complexity. Knowing all of these isn't needed when you are writing your web-applications but I do think that it builds some empathy for the people who actively work on this.

I have been exploring optimisations to the Preact diffing algortihm for quite a while, I've been a maintainer on the project now for 5'ish years (since 2019). In our v11 explorations we settled on a new diffing algorithm for Preact-children, which we refer to as skew-based diffing, this has since 10.16.0 been backported to Preact. In the future I'd love to write about everything we've discovered there but I wanted to collect my thoughts about Fragments first.