The React Fiber architecture has a lot of complexity, and if we try to read the source code, we get bogged down in the amount of code and implementation details, often learning nothing.

React Concurrency is an important application of the ReactFiber architecture. This article will not post any React source code, but use text to help you understand the ReactFiber architecture from the perspective of concurrency.

β˜•οΈ this is not an article aboutReact Concurrent modeAPI usage articles. To save space, the API usage will not be explained in detail below, only how it works. Therefore, this article assumes that the reader already knowsReact Concurrent modeRelated concepts.

🍺 In order to facilitate readers to understand, some key terms in this article are given a simple explanation in the last section

React Storage status πŸ“¦

The React codebase (V16.12.0) has been fully reconfigured using Fiber architecture and has three modes:

  • Legacy Mode (the one we are using), useReactDOM.render(...)
  • Blocking Mode, useReactDOM.createBlockingRoot(...) .render(...)
  • Concurrent Mode, useReactDOM.createRoot(...) .render(...)

However, the source code will only expose the Legacy Mode interface when compiled, because the concurrent Mode is not yet stable.

Their characteristics are as follows:

  • Legacy Mode: Synchronously Reconcile Fiber. The Reconcile task cannot be broken and executes to the end
  • Blocking Mode: synchronize Reconcile Fiber, the Reconcile task cannot be broken and will be executed to the end
  • Concurrent Mode: Reconcile Fiber is performed “concurrently”, the Reconcile task can be broken

For more details, see πŸ‘‰ : Detailed differences

Note that Concurrent Mode is nothing like multi-threaded concurrency but an illusion. Multi-threaded concurrency is multiple tasks running in multiple threads, which is true concurrency. Concurrent Mode refers to multiple tasks running in the same main thread (JS main thread), but each Task can constantly switch between the “running” and “suspended” states, giving the user the illusion of Concurrent execution of tasks. This is the same thing that CPUS do — it’s called Time Slicing.

So now we use reactdom.render (…) with the package installed via NPM I React. The React application created looks like this:

  • Fiber: The smallest processing unit for the Reconcile process of React
  • Sync: The React Reconcile process cannot be broken; it is synchronized
  • UnbatchedUpdates: In non-React events (such as setTimeout), setState cannot be batched
  • Suspense: Can only be used to load asynchronous components

To summarize: although Fiber has been refactored, it is still the same πŸ˜”

For a taste of the Concurrent pattern, see πŸ‘‰ : Adopting Concurrent Mode

The essence of Caton βŒ›οΈ

Before we do that, let’s take a look at the nature of Catton.

Monitor Refresh Rate vs. Browser Frame Rate or FPS

Refresh rate is a hardware level concept, it is constant. The refresh rate of most monitors is 60Hz, that is, data is read from the GPU Buffer every 16ms, displayed on the screen, and displayed as a screen refresh. This process is called v-sync.

Frame rate is a software-level concept, and it fluctuates. Each application has its own frame rate control strategy, which, in the case of browsers, has several considerations:

  • In order to ensure the performance and make the user not feel the lag, it will try to output image information to the GPU every 16ms
  • To reduce battery loss, reduce frame rate when unplugged
  • To reduce memory footprint, lower the frame rate when no user interaction is detected on the page
  • And so on…

Refresh rate has nothing to do with page lag. Page lag has nothing to do with frame rate.

What is Caton

It is well known that the time for light to reach the retina is about 24 milliseconds. Among other things, if light hits the retina every 16 milliseconds, the “continuous picture” is more comfortable.

Note that if it’s a static screen, we can input a frame every other day and it won’t make a difference. But in fact, most of the human-computer interaction devices we use output dynamic pictures, such as animation, scrolling, interaction, etc.

If a continuous picture is not output at the rate of 16ms/ frame, then we will feel that the picture is not continuous, which is called the lag.

Why is it stuck

This picture is calledThe pixel pipeline, which describes the process of generating chrome pixels.

We can see, the first to execute JavaScript generates DOM node, will conduct follow-up Style/Layout/Pain/Composite process, finally the image rendering. For the sake of analysis, we divide these stages into two parts:

  • JavaScript: The event loop part
  • Style/Layout/Pain/Composite: UI rendering part

Each frame of page rendering is made up of these two parts, meaning that these two parts need to complete the task within 16ms and output a frame so that the user does not feel stuck. In fact, the browser has other work to do, so it’s probably only about 10ms at most.

Therefore, whether a frame can be completed within 10ms depends on two points:

  • How long did the event cycle take
  • How long did the UI render take

This article will mainly analyze the issue of event loop, UI rendering problems see πŸ‘‰ : browser layer composition and page rendering optimization

If one or more of these two take too long, the frame will take more than 16ms to output, resulting in the browser not reaching the 60FPS frame rate, which will become a discontinuous image to the user’s eyes, resulting in a stutter.

Note that this phenomenon is called “frame rate drop”, not “frame loss”. Because frames aren’t lost, they’re just being output slower. Frame loss means that the frame rate is greater than the refresh rate of the display, so the extra frame rate is not reflected in the display, and there is no difference with lost.

The two types of problems React wants to solve 🚨

Solve CPU-intensive problems

React faces a contradiction between the fact that it wants the page to be responsive to user interaction and the following two facts:

  • The fact that “JS is single threaded”
  • The fact that rendering pages does take some CPU time

In order for the page to be responsive to user interaction, it is necessary to acquire execution rights of the main thread in a timely manner, but rendering the page does take some time for the main thread to work.

πŸ’‘, some might ask, wouldn’t React perform the “respond user interaction” task first? It’s true that the page will respond to the interaction in a timely manner, but the rendering of the page will get stuck. This will add a debounce to the page rendering, which isn’t what React wants.

Based on these two facts, there are two solutions:

  • Transfer the page rendering task to another thread, such as WebWorker
  • Allow page rendering to be paused at the right time to make way for the main thread

Why not use the first solution? Maybe you can refer to this discussion πŸ‘‰ : What are the problems with the operation of the virtual DOM with web worker multi-core parallel diff?

Solve user experience problems

The user experience has been significantly improved by solving the CPU-intensive problem. React doesn’t stop there, though. With the ability to pause, React provides a series of new apis: Suspense, SuspenseList, useTransition, useDeferredValue. Using these apis, we can optimize the user experience in a whole new dimension — faster page rendering on fast, high-performance devices, and better page experience on slow, low-speed devices.

This kind of functionality integrated into the framework is unprecedented, which is a “dimension drop” for other frameworks, but also a challenge for React itself, as the render function of React becomes increasingly difficult to understand with the advent of these apis.

For more discussion of these new apis see πŸ‘‰ how to evaluate React’s new features Time Slice and Suspense?

Suspense and the other new apis are one of the scenarios for using the React concurrency mode, and should be understood if you understand how the React concurrency mode works.

Learn about the React concurrency API and how to use it.

The key to solving the problem – pause πŸ—

By suspending the executing tasks, on the one hand, the control of the main thread is released, and the high-priority tasks are preferentially processed to solve the CPU-intensive problems. On the other hand, having Reconcile results in memory for a while and then displayed on the screen at the appropriate time provides an opportunity to solve user experience problems.

Meaning of pause

The pause is not really a pause in the execution of the code, but rather a pause that releases control of the main thread by scheduling pending tasks to the next event loop. Then, in the next event loop, the pending task is tried again to recover from the pause.

React calls this behavior interruptible-rendering

Pause implementation principle

In fact, it is not complicated, mainly divided into two parts:

  • Scheduling tasks
    • Scheduler: Is responsible for scheduling tasks, which may have different priorities
  • Perform a task
    • PerformWork: Performs tasks that process Fiber and is scheduled by Scheduler
    • Fiber: Maintains component information
    • WorkInProgress (WIP) : Is responsible for maintaining the intermediate results of the Fiber currently being processed

Let’s start with scheduling tasks:

The so-called scheduling task is to control the execution timing of the task, usually, the task will be executed one after another. But if a higher-priority task Scheduler received, at the same time, the current existing a low priority tasks are being executed, the Scheduler will “suspend” the low priority tasks, namely through the lower priority task to the next event loop and try again, so as to make the main thread to execute the high-priority task.

Due to Scheduler’s current unstable code state and React’s ongoing efforts to integrate Scheduler into the browser API, more changes to Scheduler code are likely. In addition, this code doesn’t do much to understand React and makes it difficult to read. With that in mind, this summary does not delve further into Scheduler’s code implementation, but it does provide a brief overview of Scheduler’s current state.


And then the execution:

During the first rendering of the page and subsequent updates, the scheduler schedules performWork, which iterates through the Entire FiberTree using a while loop from the current Fiber node, updating each Fiber node from top to bottom.

In traversing FiberTree, each Fiber node is processed as a unitWork, meaning that “pauses” can occur only before or after Fiber node processing begins.

The pause occurs between multiple unitworks of the performWork process, which leads to a problem: after the pause, how can we recover from the current work, instead of going through the performWork again?

React fixes this problem with a workInProgress global variable. After each unitWork execution, its result (updated Fiber) is assigned to workInProgress, which means that workInProgress always holds the result of the last unitWork. When we recover from the pause, we simply read the global variable and continue with performWork from there.

React uses Double Buffering to maintain a copy of the current Fiber. The difference is as follows:

  • FiberTree represented by workInProgress may contain both Fiber nodes that have been updated and Fiber nodes that have not been updated
  • The FiberTree represented by workInProgress is not represented on the screen, but is just a variable sitting in memory

React implements concurrency based on this mechanism. This mechanism is best described in a passage from the official documentation:

Conceptually, you can think of this as React preparing every update β€œon a branch”. Just like you can abandon work in branches or switch between them, React in Concurrent Mode can interrupt an ongoing update to do something more important, and then come back to what it was doing earlier. This technique might also remind you of double buffering in video games.

Conceptually, you can think of React as preparing every update “on a branch.” Just as you can give up work on branches or switch between them, React in Concurrent mode can interrupt an in-progress update to do something more important, and then go back to the work you were doing earlier. This technology may remind you of double buffering in video games.

In this case, a branch is a workInProgress.

The Scheduler is in the latest state

The requestAnimationFrame implementation has been removed in favor of the MessageChannel implementation. Compared with rAF’s implementation to calculate the frame length through the time interval between rAF, MessageChannel directly fixes the frame length to 5ms.

In other words, the MessageChannel implementation executes for only 5ms at a time, after which the main thread is immediately released and the remaining tasks are scheduled for the next event loop. (MessageChannel can be interpreted as setTimeout, but it’s better.)

This has the following benefits:

  • Frame length is stable. RAF implementation calculates frame length based on the execution time of rAF callback, which is very unstable, because the browser frame number will fluctuate due to various factors, resulting in a large error in frame length.
  • Better support for high refresh rate devices, since fixed frame length is 5ms, essentially assuming the browser frame rate is 5ms/1 frame, which is 1000ms/200 frames, which is a maximum frame rate of 200 frames per second.

Optimize the user experience πŸš€

With the pause mechanism described above, React eliminates CPU intensive problems, so web applications developed using React concurrency will have a better user experience. But the React team didn’t stop there. They rolled out several new apis:

  • Suspense
  • SuspenseList
  • useTransition
  • useDeferredValue

By “combining” these apis, we can optimize the user experience in a whole new dimension — faster page rendering on fast and high performance devices, and better page experience on slow and low performance devices.

Before continuing, the reader must know the usage of these apis, otherwise it will be meaningless.Copy the code

Principle of Suspense

Suspense isn’t too complicated, but you can implement Suspense components yourself. Here’s a super-simplified implementation of React Suspense:

This implementation uses the React error boundary concept. Suspense is implemented by throwing a promise inside a component wrapped in Suspense, which is then caught by componentDidCatch and processed internally. In order to realize the conditional rendering function of Suspense render function.

As a framework, however, React is more of a consideration. For example, in the minimalist example above, conditional rendering using ternary expressions inevitably results in children being unloaded, which means that the state of the child component is lost. To solve this problem, React has a special reconcile process for dealing with Suspense components:

When Suspense is suspended, i.e., rendering its fallback components, React generates two fibers at the same time, a fallbackFiber and a primaryFiber, which are used to maintain information about the fallback component and its children, respectively. This way, component state information remains in primaryFiber even if the child component is uninstalled.

UseTransition delay rendering principle

Suspense with useTransition can achieve this effect:

Child components wrapped in Suspense do not hang immediately when loading asynchronous data, but try to stay in the current state for a period of time specified by timeoutMs. If asynchronous data has been loaded within timeoutMs, the child components are directly updated to the latest state; Conversely, if timeoutMs are exceeded and asynchronous data has not finished loading, the Suspense fallback component will be rendered.

In this way, on devices with high network speed and high performance, some unnecessary loading states will disappear completely and user experience will be further optimized, which is called delayed rendering.

Delayed rendering does not delay reconcile, but rather delay reconcile results (workInProgress) rendered to the screen, to give two examples:


Example 1:

Assume that the child component starts to pull asynchronous data at 0ms by useTransition, and assume that timeoutMs is 500ms and the asynchronous data pull takes 300ms, then the whole process will look like this:

  1. At the beginning of 0ms, React performs the first Reconcile. Since asynchronous data has not been reconciled, the result of reconcile represents a fallback. The reconcile result is then stored in the workInProgress variable in memory. Assume that the reconcile phase takes 50ms, meaning that at the 50ms point in time, the Render phase is complete, and the next thing to do is to synchronize the workInProgress information to the DOM, which is the COMMIT phase. However, since we set timeoutMs, React will not commit immediately. Instead, it will wait 500ms before committing.
  2. As a result, in 300ms, the asynchronous data pull is completed and React reconcilesonceagain. Since the asynchronous data has already been loaded at this point, the components represented by the reconcile result are real child components. The reconcile result is then copied to the workInProgress variable, so the previous Reconcile result is overwritten. Suppose the Reconcile phase takes 100ms, meaning that at 400ms the Render phase is complete and the commit phase is immediately initiated, rendering the workInProgress to the page.
  3. Since the commit was committed at 400ms, nothing will be done at 500ms.

That completes the process.


Example 2:

Assume that the child component starts to pull asynchronous data at 0ms by useTransition, and assume that timeoutMs is 500ms and the asynchronous data pull takes 600ms, then the whole process will look like this:

  1. The first step is exactly the same as above.
  2. At 500ms, the asynchronous data is still not pulled, so the first commit delay is up, so React will immediately render fiber to the page, and the page will display the Fallback component.
  3. Time comes to 600ms, at which point the asynchronous data pull is completed. React reconcile again to obtain the next workInProgress, and immediately render the workInProgress to the page.

That completes the process.


These two examples confirm our conclusion that delayed rendering does not delay Reconcile, but rather delay reconcile the work progress rendered to the screen.

SuspenseList and useDeferredValue

Suspense and useTransition are easy to understand if you understand the principles of Suspense and useTransition, so I won’t go over them here.

Simple explanations of nouns

  • Fiber: The smallest unit in Reconcile. It contains component information, component status information, component relationship information, side effects list, etc. It can be interpreted as a package layer for each component.
  • FiberTree: A tree constructed from Fiber component relationship information: return(parent), Child (sibling), and Sibling (sibling). Each Fiber represents a tree, so FiberTree is essentially Fiber, and they are equivalent. Any Fiber node in FiberTree can describe the entire FiberTree through the three properties described above.
  • Reconcile:
    1. The process of reconciling Fiber updates through FiberTree is called Reconcile.
    2. This process is divided into render and COMMIT stages. The render input is Fiber and the output is updated Fiber, which is purely React level work. In the COMMIT phase, the input is updated Fiber, and the output is side effect execution and DOM update.
    3. Only the Render phase can be interrupted.
  • WorkInProgress: In the render phase above, the updated Fiber output is called workInProgress, which is essentially also a Fiber. The difference is that the workInProgress only exists in memory and is not displayed on the screen.

reference

  • The React warehouse
  • MAC warehouse
  • react-suspense-polyfill
  • React Concurrent mode
  • How do you evaluate React’s new features Time Slice and Suspense?

] (www.zhihu.com/question/26)…

  • react-fiber-architecture
  • inside-fiber-in-depth

The last

Tomorrow will be back to work, everyone must do protective measures, in the “20200202” this special day, WISH health!