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
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
and the browser's DOM tree. This idea is what originally set React
apart. React's reconciliation is what allows you to program
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
Unfortunately, those researching tree diffing in Computer Science have
not yet produced a generic algorithm with sufficient performance for
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)]].
in O(n^3)]].
Since an O(n^3) algorithm isn't going to cut it in the real-world, the
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
creators of React instead use a set of heuristics to determine what
parts of the tree have changed. Understanding how the React tree
parts of the tree have changed. Understanding the heuristics currently
diffing algorithm works in general and the heuristics currently in use
in use and how the React tree diffing algorithm works in general can
can help immensely in detecting and fixing React performance
help immensely in detecting and fixing React performance
bottlenecks. And beyond that it can help one's understanding of some
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
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
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
ways and, overall, are unlikely to change in major ways without larger
changes to React itself.
changes to React itself.
According to the [[https://reactjs.org/docs/reconciliation.html][React documentation]] their diffing algorithm is O(n)
According to the [[https://reactjs.org/docs/reconciliation.html][React documentation]] the diffing algorithm is O(n)
and based on two major components:
and is based on two major components:
- Elements of differing types will yield different trees
- Elements of differing types will yield different trees
- You can hint at tree changes with the ~key~ prop.
- 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
#+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
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
#+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
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.
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,
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
existence, and key. It does not do any other diffing. It does not diff
props. It does not diff sub-trees of modified parents.
props. It does not diff sub-trees of modified parents.
While keeping that in mind, here is an overview of the algorithm we
While keeping that in mind, here is an overview of the algorithm we’ll
will be implementing in the ~render~ method. ~element~ is the element
be implementing in the ~render~ method. ~element~ is the element from
from the current tree and ~prevElement~ is the corresponding element
the current tree and ~prevElement~ is the corresponding element in the
in the tree from the previous render.
tree from the previous render.
#+BEGIN_SRC javascript
#+BEGIN_SRC javascript
if (!element && prevElement)
if (!element && prevElement)
@ -505,21 +503,21 @@ still be invoked.
Now, to get started with our render method we must make some
Now, to get started with our render method we must make some
modifications to our previous render method. First, we need to be able
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
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
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
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
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
and to reference the DOM elements that we create. These new element
objects will have the same structure as our current elements but we
objects will have the same structure as our current elements but we’ll
will add two new properties: ~domElement~ and ~parent~. ~domElement~
add two new properties: ~domElement~ and ~parent~. ~domElement~ is the
is the DOM element associated with our synthetic element and ~parent~
DOM element associated with our synthetic element and ~parent~ is a
is a reference to the parent DOM element.
reference to the parent DOM element.
Here we begin by adding a global object that will store our last render
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
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
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
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~.
~renderTrees~.
#+BEGIN_SRC javascript
#+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
algorithm into a new function called ~render_internal~ and pass in the
result of our last render to ~render_internal~.
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
update our render method with the heuristics for reusing the DOM
elements. We name it ~render_internal~ because it is what controls the
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
~prevElement~. ~prevElement~ is a reference to the corresponding
~element~ from the previous render and contains a reference to its
~element~ from the previous render and contains a reference to its
associated DOM element and parent DOM element. If it's the first
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
~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.
delete a node that previously existed.
#+BEGIN_SRC javascript
#+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~
element. We use this observation to group the calls for ~setDOMProps~
and ~renderChildren~. Choosing when to append a new DOM element to the
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
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
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
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
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
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,
In the real React, when a new DOM element is appended to the DOM tree,
React would invoke ~componentDidMount~ or schedule ~useEffect~.
React would invoke ~componentDidMount~ or schedule ~useEffect~.
@ -612,12 +610,12 @@ function removeDOMElement(prevElement) {
}
}
#+END_SRC
#+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
creating a text element since the browser API differs slightly. We
also populate the text element's value as the API requires the first
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
argument to be specified even though later on when we set props we’ll
will set it again. This is where React would invoke
set it again. This is where React would invoke~componentWillMount~ or
~componentWillMount~ or schedule ~useEffect~.
schedule ~useEffect~.
#+BEGIN_SRC javascript
#+BEGIN_SRC javascript
function createDOMElement(element) {
function createDOMElement(element) {
@ -656,7 +654,7 @@ need to be updated or removed.
#+begin_warning
#+begin_warning
This algorithm for setting props does not correctly handle events,
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.
important and we leave it out for simplicity.
#+end_warning
#+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
number of children is decreased. The second loop starts at the first
child and then iterates through all of the children of the parent
child and then iterates through all of the children of the parent
element, calling ~render_internal~ on each child. When
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
position is passed to ~render_internal~, or ~undefined~ if there is no
corresponding element, like when the list of children has grown.
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,
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
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
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
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
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
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
~prevChild~. Otherwise this is effectively the same algorithm React
uses when rendering lists of children.
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 |
| i | child Type | prevChild Type |
|---+------------+----------------|
|---+------------+----------------|
| 0 | span | div |
| 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
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
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
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
hooks, are invoked. And invoking those lifecycle methods, or hooks, and
the whole process of tearing down and building up a component is
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
expensive. So again, if you use a bad key, like the algorithm here
simulates, you'll be hitting a major performance bottleneck since
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
The actual React implementation used to look very similar to what
we've built so far, but with React 16 this has changed dramatically
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
discrete units of work during the render process. And the React
reconciliation algorithm was changed to be based on small units of
reconciliation algorithm was changed to be based on small units of
work instead of one large, potentially long-running call to
work instead of one large, potentially long-running call to
~render~. This means that React is now able to process just part of
~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,
the render phase, pause to let the browser take care of other things,
and resume again. This is the underlying change the enables the
and resume again. This is the underlying change that enables the
experimental Concurrent Mode as well as running most hooks without
experimental Concurrent Mode as well as runs most hooks without
blocking the render.
blocking the render.
But even with such a large change, the underlying algorithms for
But even with such a large change, the underlying algorithms that
deciding how and when to render components is the same. And when not
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 does
running in Concurrent Mode, the effect is still the same, as React
the render phase in one block still. So using a simplified
still does the render phase in one block. So, using a simplified
interpretation that doesn't include all the complexities of breaking
interpretation that doesn't include all the complexities of breaking
up the process in to chunks enables us to see more clearly how the
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
process works as a whole. At this point, bottlenecks are much more
likely to occur from the underlying algorithms and not from the Fiber
likely to occur from the underlying algorithms and not from the Fibers