there have been various disputes over front-end framework for a long time. The most controversial items are the following two:
-
performance dispute
-
dispute over API design
for example, emerging frameworks come up with benchmark
to prove their excellent runtime performance, and React
is usually at the bottom of these benchmark
.
in the API
design, Vue
enthusiasts believe that "more API constrains developers and will not cause significant differences in code quality due to differences in the level of team members."
and React
enthusiasts think: " Vue
a large number of API
limits flexibility, JSX
yyds".
the above discussion boils down to the trade-off between framework performance and flexibility.
this article introduces a state management library called legendapp , which is very different from other state management libraries in terms of design philosophy.
rational use of legendapp
in React
can greatly improve the runtime performance of the application.
but the purpose of this article is not just to introduce a state management library, but to share with you changes in framework flexibility as performance improves.
performance optimization of React
It is an indisputable fact that the performance of React
is really not very good. The reason is the top-down update mechanism of React
.
with each status update, React
traverses the entire component tree first, starting from the root component.
since the traversal mode is fixed, how do you optimize performance? The answer is looking for subtrees that can be skipped when traversing .
what kind of subtree can skip traversal? Obviously a subtree of that hasn't changed.
in React
, changes are mainly caused by the following three elements:
-
state
-
props
-
context
they may change UI
, or trigger useEffect
.
therefore, if there are changes in the above three elements in a subtree, it may change, and traversal cannot be skipped.
from the perspective of changing, let's take a look at the performance tuning API in React
, for the following two:
-
useMemo
-
useCallback
The essence of is to reduce props
changes.
for the following two:
-
PureComponent
-
React.memo
Their essence is to change the way of comparing props
from congruent comparison to shallow comparison.
optimizations that the state management library can do
now that you understand the performance tuning of React
, let's take a look at what the state management library can do for performance optimization.
performance bottleneck mainly occurs during update, so there are two main directions of performance optimization:
-
reduce unnecessary updates
-
reduce the number of subtrees to traverse each update
like Redux
in the context of useSelector
takes the first path.
for the latter path, reduces the number of subtrees traversed on update usually means reduces the changes in the three elements described above.
PS:
React Forget
developed by Huang Xuan is a compiler that can generate equivalent useMemo and useCallback code. The purpose is to reduce the changes ofprops
among the three elements.
The
state management library can play a limited role in this respect, because no matter how cleverly encapsulated the state management library is, it cannot conceal the fact that he is actually operating on a React state.
for example, although Mobx
brings fine-grained updates to React
, it does not bring the performance that matches the fine-grained updates in Vue
, because Mobx
eventually triggers a top-down update.
ideas of legendapp
the legendapp
introduced in this article also takes the second path, but his idea is quite special-if you reduce the number of three elements, can not you reduce the change of three elements?
to take an extreme example, if there is no state in a large application, the entire component tree can be skipped during update.
the following is an example of a counter implemented by Hook
. useInterval
triggers a callback every second, in which an update is triggered:
function Counter() {
const [count, setCount] = useState(1)
useInterval(() => {
setCount(v => v + 1)
}, 1000)
return <div>Count: {count}</div>
}
according to the three-element rule, Counter
contains state
named count
, and if it changes every second, Counter
will not be skipped when updated (represented by Counter
render
every second).
here is an example of modification using legendapp
:
function Counter() {
const count = useObservable(1)
useInterval(() => {
count.set(v => v + 1)
}, 1000)
return <div>Count: {count}</div>
}
in this example, use the useObservable
method provided by legendapp
to define the state count
.
Counter
will only render
once, and then even if count
changes, Counter
will not render
.
how do you do this?
in the legendapp
source code, the useObservable
method code is as follows:
function useObservable(initialValue) {
return React.useMemo(() => {
//... A set of fine-grained update mechanism similar to Vue
}, []);
}
by wrapping React.useMemo
with an empty dependency, useObservable
actually returns a value that will never change.
since state
is not returned, the Counter
component does not contain any of the three elements ( state
, props
, context
), and certainly not render
.
Let's generalize this idea. If all states in the whole application are defined by useObservable
, doesn't that mean that state
does not exist in the whole application, so the whole component tree can be skipped during update?
that is to say, legendapp
implements a complete update process based on fine-grained update based on the original update mechanism of React
, and gets rid of the influence of React
as much as possible.
the principle of legendapp
next let's talk about the implementation of legendapp
status update.
in the traditional React
example:
function Counter() {
const [count, setCount] = useState(1)
useInterval(() => {
setCount(v => v + 1)
}, 1000)
return <div>Count: {count}</div>
}
count
changes, resulting in Counter
component render
, render
count
is a new value, so count
in the returned div
is a new value.
in the legendapp
example, Counter
will only render
once, and count
how to update it?
function Counter() {
const count = useObservable(1)
useInterval(() => {
count.set(v => v + 1)
}, 1000)
return <div>Count: {count}</div>
}
in fact, count
returned by useObservable
is not a number, but a component called Text
:
const Text = React.memo(function ({ data }) {
// omit the internal implementation
});
in the Text
component, it listens for changes in count
.
when count
changes, an React
update is triggered through the internally defined useReducer
.
although the update of React
traverses the entire component tree from top to bottom, only Text
components exist and change in the whole application, so all subtrees except Text
components are skipped.
tradeoff between performance and ease of use
now we know how to update the text node in legendapp
.
but JSX
is very flexible, in addition to text nodes, such as:
- conditional statement
such as:
isShow ? <A/> : <B/>
- Custom attributes
such as:
<div className={isFocus ? 'text-blue' : ''}></div>
how do you listen for changes in these forms and trigger updates?
to this end, legendapp
provides a custom component Computed
:
<Computed>
<span
className={showChild.get() ? 'text-blue' : ''}
>
{showChild.get() ? 'true' : 'false'}
</span>
</Computed>
corresponding React
statement:
<span className={showChild ? 'text-blue' : ''}>
{showChild ? 'true' : 'false'}
</span>
Computed
is equivalent to a container that listens for state changes in children
and triggers React
updates.
The Text
component corresponding to the
text node can be compared to text content wrapped by Computed:
<Computed>{text content}</Computed>
in addition, there are some more semantic tags (essentially encapsulated in Computed
), such as Show
:
for conditional statements.
<Show if={showChild}>
<div>Child element</div>
</Show>
corresponding React
statement:
{showChild && (
<div>Child element</div>
)}
there are also & lt;For/>
components for array traversal.
at this point you should notice that although we have improved runtime performance with legendapp
, we have also introduced Computed
, Show
and other new API
.
do you want the framework to be more flexible and imaginative, or would you rather sacrifice flexibility for higher performance?
this is the trade-off between performance and ease of use of that this article wants to express.
Summary
students who have used Solid.js
will find that React
introduced legendapp
is infinitely close to Solid.js
on API
.
in fact, the moment Solid.js
chooses to combine React
with fine-grained update and optimizes its performance, it determines that its final form is like this.
legendapp
+ React
has achieved high performance at run time, and if you want to optimize further, one feasible direction is compile-time optimization.
if you continue down this path, you will be infinitely close to Vue3 without abandoning the virtual Vue3.
if you go to extremes and discard virtual DOM, it will be infinitely close to Svelte
.
each framework makes a trade-off between performance and flexibility to please their target audience.