React is a JavaScript library for creating user interfaces and has its roots in functional programming. It has gained massive traction over the last 5 years due to its salient features like the ability to describe the UI in the form of components declaratively and reacting to changes quickly by working with an abstraction layer that sits on top of your target platform, in essence, called the ‘virtual DOM’.
The latest major version of React (at the time of writing) is 16 and its release brought a number of features and concepts into the mix of creating efficient and fast user interfaces with React. The most important change was definitely the complete rewrite of the reconciliation algorithm, which we will be focusing on in this article. Many other features like Error boundaries, concurrency in React, Suspense (hopefully which will release soon with React 17) and a much finer and lower level control of elements have all been made possible by Fiber. We will discuss certain pre-requisite concepts and terminologies first and then go ahead with what Fiber is on a basic level. We would then focus a little bit on concurrent rendering and suspense.
Before we go any further, I want to specify that this article assumes that the reader is aware of React in some capacity and that this is not a getting started with React guide. Another possible disclaimer would be knowing how Fiber works is not at all mandatory to develop React apps as it is an implementation detail of the library itself.
Prerequisites
Basic concept of React
React at its heart believes that a component just converts data from one form to the other and strives to be just like a pure function (not completely as React takes a pragmatic approach to solutions to make it useful in the UI world).
v = f(d)
Reconciliation
A tree of React elements is created based on whatever the render() function returns. Whenever there is an update (through the state or props et al.) a new tree is generated. React needs to diff these two trees in order to update the UI based on the changes in the new tree compared to the previous tree. This diffing is fast because of heuristics, optimizations and guidance from the developer (in the form of list keys). This is known as reconciliation (and what many people refer to when they say ‘virtual DOM'). This is at the heart of React along with the scheduler. Fiber is nothing but the current reconciliation algorithm.
Renderer
The reason for React having so many target platforms while keeping the general code and approach similar is due to the fact that it decouples the heavy lifting of state update from the application rendering on the platform. This decoupling ensures that React DOM and React Native can use their own renderers while sharing the same reconciler.
Scheduler
With the old reconciler (retrospectively named Stack), React walked the tree recursively and called render functions of the updated tree synchronously. Although this works for many cases, this approach completely disregards any sense of work prioritization. Although it does follow the data processing model as described above, React is essentially a UI library and that allows it to prioritize work and delay computations that are not essential right now. In fact, indulging in a heavier computation like network calls when a user action is pending (button click animation) can cause frames to drop and result in a poor user experience.
Delaying computations based on changes until needed by the consumer is called the pull-based approach. A pull-based approach puts the scheduling overhead on the library rather than on the developer and allows React to take advantage of back pressure and work prioritization.
Okay, enough of starters, let’s jump into the main course-
Fiber
Let’s talk about how an operating system tracks a normal program execution. The program jumps from one place to the other often with function calls etc. To keep track of all this jumping it stores the context in something called as a frame. This frame has information in it like the function it has to execute, its address, the returning address of the caller function among other things. These frames are maintained on a call stack. This allows the OS engine to schedule them optimally. A JavaScript engine also works like this with stack and stack frames and we would need a similar low-level control to achieve scheduling work in React. The only problem with this is that React cannot depend on the underlying call stack as it would keep calling the frames until it’s empty and therefore needs to break its rendering work into incremental units to have better control over scheduling. These units are essentially what’s called a fiber, which lends its name to the algorithm.
A fiber is nothing but a plain JavaScript object containing information about a component and its inputs and outputs (like a pure function that we discussed above). As a mental model, it corresponds to both the instance of a component and a stack frame. This reimplementation of the call stack allows React to have finer control over scheduling when an element is rendered.
function FiberNode( tag: TypeOfWork, pendingProps: mixed, key: null | string, mode: TypeOfMode, ) { // Instance this.tag = tag; this.key = key; this.type = null; this.stateNode = null; // Fiber this.return = null; this.child = null; this.sibling = null; this.index = 0; this.ref = null; this.pendingProps = pendingProps; this.memoizedProps = null; this.updateQueue = null; this.memoizedState = null; // much much more logic stuff }
Fibers hold information like type and key of the element, the children, siblings, parents among a lot of other attributes. Fibers allow the scheduler to assign priorities to work, pause work and come back to it later (pre-emptive cooperative scheduling), reuse previously completed work (through memoization) and in general a lower level of control.
This complete rewrite of the reconciliation algorithm was done against some 2000 tests to ensure almost complete backward compatibility, so upgrading to React 16 shouldn't break your app. Now, the whole operation was not just a technical exercise that the React core team participated in to feel smart. It facilitates the potential of other advanced features.
Concurrent rendering
Imagine that there is a page which has a form field with which the user engages commonly and there is a heavy computation (network request, filtering followed by animation) that is happening. The render of the heavy computation would take more time and thus cause frames to drop which would cause jank when the user is typing. The reason for this in the stack reconciler was that it would recursively call the render function of the tree, depth-first, during a single tick. So the heavy computation won’t relinquish control of the thread until it’s done. With fiber, this would be a low priority task as the user interaction is of utmost importance and thus it can pre-empt the heavier task and render first. This is known as time slicing in the world of concurrency and ensures that the user gets the illusion that everything is running smoothly and at the same time.
React 16 provides some experimental APIs to assign priorities to updates like:
ReactDOM.unstable_deferredUpdates() for lower priority, ReactDOM.flushSync() for higher sync priority but since the APIs are under development, the names are subject to change. At the time of writing these APIs were pretty stable but can still change in the future and fully expect the unstable tag to drop from the API names. And also notice that the renderer is leveraging the Fiber architecture to signal priorities.
Suspense
Suspense, which is currently under development, is a generic way for React to suspend displaying the component tree, until the components are fetching data and display them only after the whole tree is ready.
It allows React to suspend the render of <Post/> while the posts are being fetched over the network. It allows the developer to provide a placeholder (a loader or spinner etc.) instead of the component.
// Suspense removes the need for such kind of hacky code. // Responsibility of showing the loader and scheduling it falls // to the concerned component (and React by extension) itself. render() { return ( <FetchThing id={id}> {(isLoading, thing) => !!isLoading ? <SpinLoader /> : <SomeThing /> } </FetchThing> ) }
Since React uses Fibers instead of the native call stack, it can interrupt rendering with minimal overhead. It achieves this by simulating algebraic effects (which are beyond the scope of this article).
Conclusion
The purpose of this article was to shed light on the basic overview of some advanced concepts like the current reconciliation algorithm and new and under-development features like concurrent rendering and suspense. With these new features, React is aiming for an efficient way of controlling how and when the component rendering is scheduled. These features are also trying to bridge the gap between super-fast networks and machines from stupid-slow ones. Raising the abstraction level for such features benefit the user experience, the developer experience and, in general, the whole ecosystem.
References
https://reactjs.org/docs/design-principles.html
https://reactjs.org/docs/reconciliation.html
https://github.com/reactjs/react-basic
https://github.com/facebook/react/tree/master/packages/react-reconciler/src