Hi, I've finally gotten to the bottom of
one of the most interesting topics for me. This is how React works under the hood. Every day we use Hooks, Lifecycle, JSX,
Synthetic Events, and confidently talk about the performance
of our project. This confidence was generated by excellent
React documentation, and just a huge number of hours spent writing projects on this technology. But the question is how well we understand
what is really going on inside React. how we get 60 frames per second even with a huge
number of interactive elements on the screen.
That's why I chose React Reconciliation
as the first topic for this playlist. As a source of information, I used the official
React documentation and an article by Andrew Clark, co-founder of the Redux. He presented similar speech at
the React Next 2016 conference. Andrew works for the React core team, therefore
there is definitely confidence in this material.
All links will be in the description. I would start with a brief definition from the
article Reconciliation is the algorithm behind what is popularly understood as the virtual DOM. Let's try to understand in
more detail how it works. It all starts with the basic unit, the node. From them, using a set of render methods, a
whole tree of react elements is assembled, which describes the entire state of the
application and is stored in memory. And this tree is called current. Then this tree is fed into
the rendering environment. In the case of the web, this
This environment will transform this tree into a set of DOM operations
needed to render the current changes. It prioritizes DOM operations according to
the urgency of their getting to the user. When the website is first loaded, the rendering
environment renders the entire DOM tree, and the user can finally see the first result of all these manipulations and
can press the "Hi" button.
Clicking the button leads to a change
in our website, and as a result, a new tree is built, and
it is called Work-in-Progress. Next, we compare the work-in-progress tree with
the current tree and calculate the difference between them, and only the difference between
these trees is given to the rendering environment and then the already familiar process
of turning the difference into a set of DOM operations
and prioritizing them. And as a result, these operations
will update the DOM tree of our website and the user will finally
see that the bruise greeted him. Oh yes, I almost forgot, the work-in-progress
tree becomes the current tree. This is a fairly general instruction describing
the mechanism of reconciliation, but it has many interesting details. Let's try to go through some of them. For example, React core developers have divided
the tree comparison engine and the rendering environment into separate phases, so that React
DOM and React Native can use their own rendering engines when using the same comparison
engine that is in React core. The next point is to work with tree. There are several general solutions to the
algorithmic problem of transforming one tree into another with a minimum number of operations. However, advanced algorithms have a
complexity of the order of O(n^3), where n is the number
of elements in the tree. If 1 ,000 nodes were mounted, this algorithm
would require a billion comparisons. This is too expensive. So React core developers chose a heuristic
algorithm of the order of O(n). With it, 1 ,000 nodes requires 1 ,000 comparisons. It sounds much more attractive. Let's figure out what a heuristic algorithm is. Heuristic is a technique designed for solving
a problem more quickly, when classic methods are too slow, or for finding an approximate
solution, when classic methods fail to find any exact solution. Raveli speaking, this algorithm is not effective
for all cases, and the React core team has made several assumptions to
make this algorithm work. First, two elements with different
types will produce different trees. Second, the developer can specify which child
elements can remain stable between different renderers using the key property, however, in
practice, these assumptions are true for most of the cases. For example, in the following code, diff
is the parent and the counter is the child. And then there were updates, and we changed the
type of the parent element from diff to span and the child remained the same component counter. Despite this, diff and span are different types,
and the entire tree starting from this element will be unmounted and their state will
be destroyed and then mounted again. Let's think about what it means
for us as a React developers. For example, we have a pure component that
is responsible for displaying a picture. Perhaps it has some kind of logic, for example
checking for its presence in the browser cache.
And if the cache doesn't include it, we need to
show a stubbed picture during the loading state, or maybe some other logic inside. This component is used in
many places of the project. For instance, in an avatar. The user has the opportunity to
switch his avatar to the editing mode. It means that controls appear next to it. In our case, it is to delete the
picture or to upload a new one. Of course, we expect that when switching from
the avatar view mode to the edit mode, the image component itself will not be updated because
it is a pure component and we pass the same props
to it. But it's not so simple. We need to wrap the image
component in an additional diff in order to do absolute positioning the controls on top of the
picture itself in the corner. As a result of switching the mode, the algorithm
begins to revise our components from top to bottom. The first was a diff, and it remained. Then there was the image component,
and now again some kind of diff. So there was a change of types. This means that the past image component
is completely removed from the tree. It means ComponentWillOnMount is
called from the previous instance. And then the tree is rebuilt, which means that
a new instance is created for the image class.
And RenderMethod and ComponentDidMount are
called, but we didn't count on it at all. To fix this problem, let's
just remove the extra diff. Let's say we figured out how to implement
it without the diff, and add only controls and editing
mode. In this case, it will really work. Our image component will not be
mounted and unmounted every time. But then, I decided to experiment and added
controls, not under the image, but above it. It is small changes, but again, the problem
returned with the fact that the image component was unmounted and mounted when switching the mode. And it sounds logical, we again compare from top
to bottom, and see where the picture was, now the controls, so it was defined as a change
in types, and again everything went wrong. At the same time, if you change the code a little,
and bring it to the form with single return, and use a logical operator, then everything works. It works as now the place is reserved for boolean,
although it is not displayed to the user, or for the controls, but the position of
the image component is always the same. But let's return to the case with two returns. To solve this problem, we can
use the second assumption. The developer can specify which
child elements can stay the same between different renders
using property key. It means if we specify the key property for the
image component, then React will know that the position of the component has changed, and your
code will work again as you originally expected.
And of course, the first thought was
to try to use the key and wrap the editing case in an additional
diff, but it doesn't work. Key works only within the
same depth level of nodes. Knowing these assumptions, you can easily solve
the problem with strange behavior at first glance. But do not forget, thanks to this assumption, React was able to speed up
the algorithm for traversing the tree from the order of the
O(n^3) to the O(n) order. And it is very important. The next very important thing that
was mentioned in this video is the prioritization of DOM
operations. This is really a very important and very
complex thing, because just think about it. We want the application not to lag, as in games we expect 60 frames
per second for a smooth picture.
To achieve it, you need to update
the frames every about 16 ms. Now imagine that the user has made
a 240 ms interface update. To prevent all this from starting to lag, the
React Core team decided to split the interface update into smaller DOM operations, the
so-called units, and prioritize them. Because the hover animation of the button seems
to be a higher priority than updating data in a block that is not even in
the viewport at the moment. Therefore, React sorted the
tasks for you and showed the animation in the first frames and postponed
some less prioritized tasks for the next frames. To implement this logic, the React team uses
two methods, requestAnimationFrame for higher priority tasks that need to be performed literally
in the next frame, and requestIdleCallback for low priority tasks that can be
performed while the application is idle. More details can be found at
the links in the description. As you see, the main difficulty of such a task
is to priorities these small DOM operations.
You can even find a separate package
in React Repository called scheduler. If we open it, we will see
a field called scheduler priorities.js, which includes priority
constants, which we discussed earlier. As you can see, there is
an idle priority constant. It means this kind of task is performed
while the application is idle. There is a low normal priority and others. All this allows you to determine
the order of DOM operations. And if we go back and open the
scheduler.js file and scroll a little, we will even see the timeouts
of these priorities. The numbers look pretty high, as much as 10
seconds of waiting for low priority tasks. And let's scroll a little lower. There is, we see the timeouts returns
depending on the priorities of the task. But I'm not sure if this code is currently used
in production, or if it's just some drafts. Since we see prefix unstable
on many exported methods, And if you search globally by the repository, you will see that priority constants
is duplicated in several places. Probably, active work is still
underway to find the best solutions. The last point which I would like to talk
quite a bit is about the history of the React. In version 16, the long-awaited project called
Fiber was finally introduced into React, which made it possible to plan which operations with
the DOM need to be performed now, which can be postponed a little, and which are no longer
relevant at all and cannot be performed. and as a result, the user gets his
cherished 60 frames per second. This algorithm is, of course, much
more than just a React scale scheduler. It started as an experiment and was written from
scratch, and after two years it was merged into the repository, which
personally made me very happy. I remember there was even
an own site with countdown to the release of Fiber was introduced.
What's it all about? This is an example where simple
migration of your project to a newer version of the library can
improve your project's performance. Therefore, it is always worth keeping an eye out
for new experiments from your favorite libraries and perhaps you can benefit your current
project without spending a lot of money on it. If you liked this video, I will be grateful if
you press like button and subscribe to the channel because this is not the last video and many
other useful things await you on our channel. Bye bye.