简体   繁体   中英

What is the motivation behind React's “diffing” Heuristic algorithm?

My question is about the Motivation for implementing a heuristic O(n) algorithm .

There are some generic solutions to this algorithmic problem of generating the minimum number of operations to transform one tree into another. However, the state of the art algorithms have a complexity in the order of O(n^3) where n is the number of elements in the tree.

  • Why transforming one tree to another have a complexity of O(n^3)?

If we used this in React, displaying 1000 elements would require in the order of one billion comparisons. This is far too expensive. Instead, React implements a heuristic O(n) algorithm based on two assumptions:

  • Two elements of different types will produce different trees.
  • The developer can hint at which child elements may be stable across different renders with a key prop.
  • Can you elaborate on what is heuristic in React's implementation?
  • Do the assumptions make it O(n) in an average case?
  • There is a transformation only based on assumptions
  • Two elements of different types will produce different trees.
  • The developer can hint at which child elements may be stable across different renders with a key prop.

The whole tree is not re-rendered if key do not change or no new elements are added

  • This management is based on assumption by experience, so it is heuristic

Current state-of-the-art diffing algorithms have a complexity of O(n^3) (n number of nodes) to find the minimum amount of transform operations needed to transform a tree into another tree. But this complexity, as React docs mention , can be too high and in usual cases it is not needed.

That is why React uses a heuristic that on average takes O(n) (linear time).

  1. Two elements of different types will produce different trees.
  2. The developer can hint at which child elements may be stable across different renders with a key prop.

Being a heuristic means that there are cases where the diff may produce more transformations than necessary (it is not optimal in general), however it can be optimal in usual and often-used cases where the two algorithms (optimal and heuristic) can produce exactly same results (with heuristic taking less time to generate), or differences between the two algorithms have minimum impact on performance.

PS:

Why transforming one tree to another have a complexity of O(n^3)?

In order to answer this question one has to look into state-of-the-art algorithms. But in general the answer is that many comparisons (between nodes and their children) have to be made in order to find the minimum number of necessary transformations .

There are pretty good reasons why the React's diff algorithm is the way it is, but the documented "motivation" doesn't really make enough sense to be the real truth.

Firstly, while it's certainly true that an optimal "tree diff" takes O(N 3 ) time, a "tree diff" algorithm is not the single best alternative to what React actually does, and in fact doesn't really fit will into react's rendering process at all. This is mostly because, in the worst case, rendering a react component produces a list (not a tree) of react elements that needs to be matched up against a list of preexisting components.

There is no new tree when the diff is performed, because the new list needs to be matched against the preexisting tree before the children of the new elements are rendered. In fact, the results of the diff are required to decided whether or not to re-render the children at all.

So... In matching up these lists, we might compare the React diff against the standard Longest-Common-Subsequence algorithm, which is an O(N 2 ) algorithm. That's still pretty slow, and there is a performance argument to be made. If LCS was as fast as the React diff then it would have a place in the rendering process for sure.

But, not only is LCS kinds slow, it also doesn't do the right thing . When React is matching the list of new elements up against the old tree, it is deciding whether or not each element is a new component, or just a prop update to a pre-existing component. LCS could find the largest possible matching of element types, but the largest possible matching isn't necessarily what the developer wants .

So, the problem with LCS (or a tree diff, if you really want to push the point), is not just that it's slow, but that it's slow and the answer it provides is still just a guess at the developer's intent. Slow algorithms just aren't worth it when they still make mistakes.

There are a lot of other fast algorithms, though, that React developers could have used that would be more accurate in most cases, but then the question is "Is it worth it?" Generally, the answer is "no", because no algorithm can do a really good job of guessing a developer's intent, and guessing the developer's intent is actually not necessary .

When it's important to a developer that his new elements are properly matched up to his preexisting components so they don't have to rerender, then the developer should make sure that this is the case. It's very easy -- he just has to provide key props when he renders a list. Developers should pretty much always do this when rendering a list, so that the component matching can be perfect , and they don't have to settle for any kind of guess.

React will produce a warning if you don't put in key props where they are required to make the matching unambiguous, which is far more helpful than a better diff. When you see it you should fix your components by adding the proper key props, and then the matching will be perfect and it doesn't matter that there are other algorithms that could do a better job on badly written components.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM