React Rendering Order
I was recently asked to measure & track the performance of a React component (and all its sub-components) as part of a huge refactoring project the company had undertaken. In a nutshell, we wanted to track how long the component takes until its rendering is complete. Since the component was made up of a number of smaller sub-components, many of these connected to the Redux store and fetching data asynchronously, it was important to understand how the React rendering algorithm works. I have already written about my learning experience when measuring the time each component takes to render which goes into greater detail about the algorithm itself; while this blog post takes a very simplified and high-level overview of the order in which components are rendered and re-rendered using examples.
To demonstrate the order in which React components are rendered, we have created a simple component tree and tagged each component with a unique ID.
By adding a
React.Profiler component to each
Component we are able to measure when each component renders. The sequence for the above component tree is
beginWorkand a component's rendering is complete (
completeWork) only once all its children's rendering is complete. As a result, the root component in your tree will always be the last one to complete render.
You may experiment with the source code if you wish.
But what about connected components & async rendering?
Very often (as was our case) components and sub-components are connected to Redux store or asynchronously fetching data from an API. In some cases, we are also using the render prop technique, in which case data is fetched by a parent component and then passed down to its children. In these cases, how does the React reconciliation algorithm behave?
In the above example,
Container simulates a component which fetches data asynchronously, while
RenderProps simulates a component which fetches data asynchronously and then passes this to its children as a prop (
prefix); some of which render conditionally based on its value (initially false). In both cases, the
timeout prop is used to define how long the asynchronous event would take until the data is "fetched" and it is only there for demonstration purposes as it has no impact on our test.
Similarly to the previous example, we are able to determine when each component finishes rendering thorugh the use of
React.Profiler. Initially, the components will render based on the same rules as above, depth-first traversal and all children must complete render.
Another 1000ms later and component C2 now resolves. Similarly to C3, its data is fetch and re-rendered. Additionally, it will also pass the render prop
prefix to its children and the conditional render is now truthy. The resultant render complete order is as follows:
You may experiment with the source code for the above example too.
So which is the last render?
Using the above information, we were able to confidently say that the entire component tree is ready from rendering when our root node (A0 in the example above) has rendered for the last time. Unless within a finite amount of time, measuring the "last" of anything is difficult as on each iteration you do not know if there will be a successive one. To solve this, we looked and imitated how Largest Contentful Paint works, as it has a similar challenge (how do you know an element is the largest if you don't know what's coming next?). Ultimately, the solution was relatively straightforward as we created a
performance.mark for each render of our root component. The last mark is the last render and each previous mark was the last render until that point.
The final piece of the puzzle was to send this data to the performance monitoring tool we were using. In our case it is SpeedCurve, which provides an API; but the same approach used by SpeedCurve works for Google Analytics or other RUM tools. Using the non-blocking sendBeacon() API on
unload and on history change (if your app is a SPA); you could POST the timings of the last
performance.mark to an endpoint.
And that's a wrap 🌯. Thank you for reading and shout out to @maxkoretskyi for his fantastic articles on the topic.