Skip to content

Fiber Principles: Contributing To Fiber #7942

Closed
@sebmarkbage

Description

@sebmarkbage

I just wanted to document a few unique design patterns that apply to Fiber, but not necessarily anything else. I'll start here.

  • You may mutate the fiber that you're working on during beginWork and completeWork phases but you may not have any other global side-effects. If you need a global side-effect, that have to be moved to the commitWork phase.
  • Fiber is a fixed data structure. It shares the same hidden class. Never add fields outside of construction in ReactFiber.
  • Nothing in the reconciler uses dynamic dispatch. I.e. we don't call a first class function, except for user code such as ref callbacks, functional components, render methods, etc. The rest is a static function available in a closure. I.e. use myHelper(obj) instead of obj.myHelper(). Any time we need to branch logic we use a switch statement over a tag which is a number that indicates which type of object we're dealing with and which branch to take (see pattern matching).
  • Many modules are instantiated with a HostConfig object. It is a single constructor that gets called on initialization time. This should be inlinable by a compiler.
  • Nothing in Fiber uses the normal JS stack. Meaning it does use the stack but it can be compiled into a flat function if needed. Calling other functions is fine - the only limitation is that they can't be recursive.
  • If I can't use recursion, how do I traverse through the tree? Learn to use the singly linked list tree traversal algorithm. E.g. parent first, depth first:
let root = fiber;
let node = fiber;
while (true) {
  // Do something with node
  if (node.child) {
    node = node.child;
    continue;
  }
  if (node === root) {
    return;
  }
  while (!node.sibling) {
    if (!node.return || node.return === root) {
      return;
    }
    node = node.return;
  }
  node = node.sibling;
}

Why does it need to be this complicated?

  • We can use the normal JS stack for this but any time we yield in a requestIdleCallback we would have to rebuild the stack when we continue. Since this only lasts for about 50ms when idle, we would spend some time unwinding and rebuilding the stack each time. It is not too bad. However, everything along the stack would have to be aware of how to "unwind" when we abort in the middle of the work flow.
  • It is plausible we could do this at the level of OCaml algebraic effects but we don't currently have all the features we need and we don't get the performance tradeoffs we want out of the box atm. This is a plausible future way forward though.
  • Most code lives outside of this recursion so it doesn't matter much for most cases.
  • Most of what React does is in the space of what the normal stack does. E.g. memoization, error handling, etc. Using the normal stack too, just makes it more difficult to get those to interact.
  • Everything we put on the stack we generally have to put on the heap too because we memoize it. Maintaining the stack and the heap with the same data is theoretically less efficient.
  • That said, all of these optimizations might be moot because JS stacks are much more efficient than JS heaps.
  • One thing that I wanted to try was to compile React components to do work directly on these data structures, just like normal programming languages compile to make mutations etc. to the stack. I think that's where the ideal implementation of React is.

Let's just try it and see how it goes. :D

cc @spicyj @gaearon @acdlite

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions