A tale of two trees. These are the two trees that people most often
This is a tale of two trees, the two trees that people most often
talk about when talking about React's "secret sauce": the virtual DOM
and the browser's DOM tree. This idea is what originally set React
apart. React's reconciliation is what allows you to program
@ -444,48 +442,48 @@ diffing" algorithm.
Unfortunately, those researching tree diffing in Computer Science have
not yet produced a generic algorithm with sufficient performance for
use in something like React; as the current best algorithm still [[https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf][runs
use in something like React, as the current best algorithm still [[https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf][runs
in O(n^3)]].
Since an O(n^3) algorithm isn't going to cut it in the real-world, the
creators of React instead use a set of heuristics to determine what
parts of the tree have changed. Understanding how the React tree
diffing algorithm works in general and the heuristics currently in use
can help immensely in detecting and fixing React performance
parts of the tree have changed. Understanding the heuristics currently
in use and how the React tree diffing algorithm works in general can
help immensely in detecting and fixing React performance
bottlenecks. And beyond that it can help one's understanding of some
of React's quirks and usage. Even though this algorithm is internal to
React and can be changed anytime its details have leaked out in some
ways and are overall unlikely to change in major ways without larger
React and can be changed anytime, its details have leaked out in some
ways and, overall, are unlikely to change in major ways without larger
changes to React itself.
According to the [[https://reactjs.org/docs/reconciliation.html][React documentation]] their diffing algorithm is O(n)
and based on two major components:
According to the [[https://reactjs.org/docs/reconciliation.html][React documentation]] the diffing algorithm is O(n)
and is based on two major components:
- Elements of differing types will yield different trees
- You can hint at tree changes with the ~key~ prop.
In this section we will focus on the first part: differing types.
In this section we’ll focus on the first part: differing types.
#+begin_note
In this book we won't be covering keys in depth but you will see why
In this book we won't be covering keys in depth, but you’ll see why
it's very important to follow the guidance from React's documentation
that keys be: stable, predictable, and unique.
that keys are stable, predictable, and unique.
#+end_note
The approach we will take here is to integrate the heuristics that
The approach we’ll take here is to integrate the heuristics that
React uses into our ~render~ method. Our implementation will be very
similar to how React itself does it and we will discuss React's actual
similar to how React itself does it and we’ll discuss React's actual
implementation later when we talk about Fibers.
Before we get into the code changes that implement the heuristics it
Before we get into the code changes that implement the heuristics, it
is important to remember that React /only/ looks at an element's type,
existence, and key. It does not do any other diffing. It does not diff
props. It does not diff sub-trees of modified parents.
While keeping that in mind, here is an overview of the algorithm we
will be implementing in the ~render~ method. ~element~ is the element
from the current tree and ~prevElement~ is the corresponding element
in the tree from the previous render.
While keeping that in mind, here is an overview of the algorithm we’ll
be implementing in the ~render~ method. ~element~ is the element from
the current tree and ~prevElement~ is the corresponding element in the
tree from the previous render.
#+BEGIN_SRC javascript
if (!element && prevElement)
@ -505,21 +503,21 @@ still be invoked.
Now, to get started with our render method we must make some
modifications to our previous render method. First, we need to be able
to store and retrieve the previous render tree. Then we need to add
code to compare parts of the tree to decide if we can re-use DOM
to store and retrieve the previous render tree. Then, we need to add
code to compare parts of the tree to decide if we can reuse DOM
elements from the previous render tree. And last, we need to return a
tree of elements that can be used in the next render as a comparison
and to reference the DOM elements that we create. These new element
objects will have the same structure as our current elements but we
will add two new properties: ~domElement~ and ~parent~. ~domElement~
is the DOM element associated with our synthetic element and ~parent~
is a reference to the parent DOM element.
objects will have the same structure as our current elements but we’ll
add two new properties: ~domElement~ and ~parent~. ~domElement~ is the
DOM element associated with our synthetic element and ~parent~ is a
reference to the parent DOM element.
Here we begin by adding a global object that will store our last render
tree, keyed by the ~container~. ~container~ refers to the browser's
DOM element that will be the parent for all of the React derived DOM
elements. This parent DOM element can only be used to render one tree
of elements at a time so it works well to use as a key for
of elements at a time, so it works well to use it as a key for
~renderTrees~.
#+BEGIN_SRC javascript
@ -537,16 +535,16 @@ As you can see, the change we made is to move the core of our
algorithm into a new function called ~render_internal~ and pass in the
result of our last render to ~render_internal~.
Now that we have stored our last render tree we can go ahead and
Now that we have stored our last render tree, we can go ahead and
update our render method with the heuristics for reusing the DOM
elements. We name it ~render_internal~ because it is what controls the
rendering but takes an additional argument now: the
rendering, but it now takes an additional argument: the
~prevElement~. ~prevElement~ is a reference to the corresponding
~element~ from the previous render and contains a reference to its
associated DOM element and parent DOM element. If it's the first
render or if we are rendering a new node or branch of the tree then
render or if we are rendering a new node or branch of the tree, then
~prevElement~ will be ~undefined~. If, however, ~element~ is
~undefined~ and ~prevElement~ is defined then we know we need to
~undefined~ and ~prevElement~ is defined, then we know we need to
delete a node that previously existed.
#+BEGIN_SRC javascript
@ -589,11 +587,11 @@ render its children is when we are deleting an existing DOM
element. We use this observation to group the calls for ~setDOMProps~
and ~renderChildren~. Choosing when to append a new DOM element to the
container is also part of the heuristics. If we can reuse an existing
DOM element then we do, but if the element type has changed or if
there was no corresponding existing DOM element then and only then do
DOM element, then we do this, but if the element type has changed or if
there was no corresponding existing DOM element, then, and only then, do
we append a new DOM element. This ensures the actual DOM tree isn't
being replaced every time we render, only the elements that change are
replaced.
being replaced.
In the real React, when a new DOM element is appended to the DOM tree,
React would invoke ~componentDidMount~ or schedule ~useEffect~.
@ -612,12 +610,12 @@ function removeDOMElement(prevElement) {
}
#+END_SRC
In creating a new DOM element we just need to branch if we are
In creating a new DOM element, we just need to branch if we are
creating a text element since the browser API differs slightly. We
also populate the text element's value as the API requires the first
argument to be specified even though later on when we set props we
will set it again. This is where React would invoke
~componentWillMount~ or schedule ~useEffect~.
also populate the text element's value, as the API requires the first
argument to be specified even though later on when we set props we’ll
set it again. This is where React would invoke~componentWillMount~ or
schedule ~useEffect~.
#+BEGIN_SRC javascript
function createDOMElement(element) {
@ -656,7 +654,7 @@ need to be updated or removed.
#+begin_warning
This algorithm for setting props does not correctly handle events,
which must be treated specially. For this exercise that detail is not
which must be treated specially. For this exercise, that detail is not
important and we leave it out for simplicity.
#+end_warning
@ -665,7 +663,7 @@ elements that are no longer being used. This would happen when the
number of children is decreased. The second loop starts at the first
child and then iterates through all of the children of the parent
element, calling ~render_internal~ on each child. When
~render_internal~ is called the corresponding previous element in that
~render_internal~ is called, the corresponding previous element in that
position is passed to ~render_internal~, or ~undefined~ if there is no
corresponding element, like when the list of children has grown.
@ -692,7 +690,7 @@ It's very important to understand the algorithm used here because this
is essentially what happens in React when incorrect keys are used,
like using a list index for a key. And this is why keys are so
critical to high performance (and correct) React code. For example, in
our algorithm here, if you removed an item from the front of the list
our algorithm here, if you removed an item from the front of the list,
you may cause every element in the list to be created anew in the DOM
if the types no longer match up. In this book we won't be
incorporating keys, but it's actually only a minor difference in
@ -700,7 +698,7 @@ determining which ~child~ gets paired with which
~prevChild~. Otherwise this is effectively the same algorithm React
uses when rendering lists of children.
#+CAPTION: Example of ~renderChildren~ 2nd loop when the 1st element has been removed. In this case the trees for all of the children will be torn down and rebuilt.
#+CAPTION: Example of ~renderChildren~ 2nd loop when the 1st element has been removed. In this case, the trees for all of the children will be torn down and rebuilt.
| i | child Type | prevChild Type |
|---+------------+----------------|
| 0 | span | div |
@ -709,8 +707,8 @@ uses when rendering lists of children.
There are a few things to note here. First, it is important to pay
attention to when React will be removing a DOM element from the tree
and adding a new one as this is when the related lifecycle events or
hooks are invoked. And invoking those lifecycle methods or hooks, and
and adding a new one, as this is when the related lifecycle events, or
hooks, are invoked. And invoking those lifecycle methods, or hooks, and
the whole process of tearing down and building up a component is
expensive. So again, if you use a bad key, like the algorithm here
simulates, you'll be hitting a major performance bottleneck since
@ -724,24 +722,24 @@ tearing down and rebuilding the trees of child components.
The actual React implementation used to look very similar to what
we've built so far, but with React 16 this has changed dramatically
with the introduction of Fibers. Fibers are a name that React gives to
with the introduction of Fibers. Fibers is a name that React gives to
discrete units of work during the render process. And the React
reconciliation algorithm was changed to be based on small units of
work instead of one large, potentially long-running call to
~render~. This means that React is now able to process just part of
the render phase, pause to let the browser take care of other things,
and resume again. This is the underlying change the enables the
experimental Concurrent Mode as well as running most hooks without
and resume again. This is the underlying change that enables the
experimental Concurrent Mode as well as runs most hooks without
blocking the render.
But even with such a large change, the underlying algorithms for
deciding how and when to render components is the same. And when not
running in Concurrent Mode the effect is still the same as React does
the render phase in one block still. So using a simplified
But even with such a large change, the underlying algorithms that
decide how and when to render components are the same. And, when not
running in Concurrent Mode, the effect is still the same, as React
still does the render phase in one block. So, using a simplified
interpretation that doesn't include all the complexities of breaking
up the process in to chunks enables us to see more clearly how the
process as a whole works. At this point bottlenecks are much more
likely to occur from the underlying algorithms and not from the Fiber
process works as a whole. At this point, bottlenecks are much more
likely to occur from the underlying algorithms and not from the Fibers