Front-end framework: a trade-off between performance and flexibility

May 29, 2023 1248hotness 0likes 0comments

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 of props 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 .

online Demo

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.

InterServer Web Hosting and VPS

Aaron

Hello, my name is Aaron and I am a freelance front-end developer

Comments