60 frames per second (fps), this number not only represents the refresh rate of a typical pc monitor, but also the time constraint an application must adhere to if it’s to be perceived as being smooth and performant. 60 fps means there are only 1 frame/60 fps or 16.66ms for an application to produce a frame and for the browser to take that frame and paint it on the screen. According to Google, excluding the time needed for the browser to paint the frame leaves only about 10ms. In financial technology, namely in real-time financial applications, not adhering to these rendering constraints would exacerbate performance issues. In these types of applications, constantly streaming prices and market data must be taken to update the components that display the financial instrument’s information, grids that reflect updated positions, and charts that reflect current market movements. Users of these applications expect, perhaps more so than most users of other types of applications, that everything be instantaneously updated.
According to Google and PubNub, response times greater than 1 second will cause a user to start losing focus in your application. Greater than 10 seconds would cause users to become frustrated and could even cause them to dismiss your application completely. It is clear that as web applications become larger and more complex, performance is something that needs to be kept under close watch. However, before optimizing, it is important to first understand how the browser works, what affects its performance, and when your application is in need of a performance boost.
How The Browser Works
When you navigate to a webpage, the browser will take the code for that site through something called the critical rendering path. The browser starts by parsing the HTML code to build the DOM tree and the css to build the CSSOM tree. The former represents the hierarchy of HTML elements of the webpage and the latter represents the styles applied to those elements. Together with these steps, the browser must also fetch and execute any javascript code before finalizing the construction of the DOM. The browser will then combine these trees to form the render tree, which represents only the styled nodes that will be rendered on the webpage. This render tree is used during the layout stage, also called reflow, where the browser will calculate the positions, sizes, and geometry of the elements. The final stage is called painting, where the browser uses the information gained from the layout stage to convert each node in the render tree into actual pixels. Continuous reads and writes to the DOM causes the browser to repeatedly go through the layout and paint stages. This is known as layout thrashing or forced synchronous layout, which can have a huge impact on performance.
When to Optimize
To improve performance you must simply do less work, and the work that you do must be done in less time. In order to achieve this, work can be deferred to a later time to improve loading times, less code can be written so there’s less to process, and heavy computations and objects can be kept in memory to avoid as many unnecessary updates as possible. However, optimization techniques shouldn’t be used prematurely since their effect could be negligible, or even detrimental, and your time could be better spent elsewhere. Performance optimization techniques should be used when it’s certain that a process will have a noticeable impact or after an issue has been noticed and measurements have been taken. In general, it is important to first measure, and then seek to optimize. However, It’s not always necessary to implement performance optimization techniques yourself since frameworks such as React have some optimization techniques already built-in.
How React Works
According to Dan Abramov, “React programs usually output a tree that may change over time.” He calls that tree a “Host Tree” as it’s outside of react and represents a host environment. The smallest building blocks of these host trees are nodes, or “host instances.” In the browser, the host tree would be the DOM and the host instances would be DOM nodes. React also utilizes renderers, such as React DOM, which act as the bridge between React and the host environment. The smallest building block in React is the React element, which is a javascript object that simply describes a host instance. These objects are immutable, they are tossed away and new ones are created in their place whenever the UI is supposed to change. React elements also form a tree that describes the host tree, and when a render occurs React will update the host tree so that it matches the React tree. The process of determining which host instances need updating is called reconciliation.
React's Built-in Optimization
Reading and then immediately writing to the DOM forces the browser to reflow. This has a heavier impact on performance than first doing all of the reads to the DOM and then all of the writes because doing each separately allows the browser to defer these actions until the end of the current operation. This is why React uses reconciliation to efficiently determine which elements need to be updated on the DOM and re-rendered, which vastly reduces the number of times the DOM needs to be read from and written to. React also has other optimization techniques such as batching multiple state updates into a single update to prevent multiple re-renders. When using the useState() hook, more on hooks later, React will also bail out of a re-render caused by a state change if the new state value is equal, using Object.is() for equality check, to the previous state value. This is not the case when using its class component equivalent setState() as every call to it will cause a re-render, unless its new state value comes from a passed function that returned null. There are times however, when react’s built-in techniques are not enough, causing us to seek out other tools to improve the performance of our applications.
Rendering Optimization
shouldComponentUpdate
React lifecycle methods can help in improving the performance of your application. In particular, the shouldComponentUpdate() lifecycle method is called by React prior to calling the component’s render method. Here you may check whether or not a component should be re-rendered. This method only exists for performance optimization and returning false will currently not trigger a re-render. However, according to the React docs, “in the future React may treat shouldComponentUpdate() as a hint rather than a strict directive, and returning false may still result in a re-rendering of the component.” instead of writing your own shouldComponentUpdate() however, it is recommended to have your class component extend PureComponent, which implements shouldComponentUpdate() and does “a shallow comparison of props and state, and reduces the chance that you’ll skip a necessary update.”
React.memo
React.memo is a higher order component that takes your component as its first argument and memoizes its props, doing a shallow comparison to determine if the component should re-render. What this means is that if the props passed to your component are referentially equal, React will skip over reconciling that component. React.memo is the functional component equivalent of PureComponent, but it differs in that it only considers prop changes. If the shallow comparison is not adequate for your needs, you may also pass a custom comparison function as the second argument. However, using React.memo with components that take many props would result in a net loss in performance since you’re simply adding more computations that would most likely still result in a re-render. In most cases, the preferred alternative to using React.memo is component composition. Utilizing React’s built in children property, for example, within a component would result in maintaining the referential equality of the child component without the overhead that comes with using React.memo.
useCallback & useMemo
useCallback() and useMemo() are React hooks that return memoized functions and values, respectively. Hooks are simply functions that allow function components to utilize state and other React features which, in the past, were only possible with class components. These two hooks are the same, useCallback() is simply a shorthand for using useMemo() when it returns a function. They both take a callback function as their first argument and an array of dependencies as their second. When these dependencies change, the passed callback function will run, thus returning an updated value. This happens while the component is rendering, it’s important to keep that in mind so that no side effects occur inside of these hooks since that will cause more re-renders and decrease performance. useCallback() should only be used when the returned function needs to retain reference equality, an example being when the function is passed as a prop to an optimized child component. The same applies to useMemo(), but useMemo() may also be used to avoid an expensive computation on every render. It’s important to note that using useCallback() still requires memory to be allocated for the function definition, not to mention that useCallback() would also need to be called. Using this function as an attempt to reduce the number of times a function gets created on every render would actually result in a decrease in performance as more work needs to be done.
Example
Let’s say we have an optimized function component, RenderCounter, that should only render when its props have changed. All this component does is count the number of times it has been rendered and displays that count on screen. The Counter component is passed as an argument to React.memo to achieve the prop memoization. This should mean the component won’t re-render unless its props change, however, that’s not necessarily the case. Notice that on line 13 the prop passed to our Counter component is a function. Since React.memo does a shallow comparison, it will only check if the reference to the function onClick has changed. Now let’s say that this component’s parent looks like this:
At first glance you may not notice anything wrong with this component. All it does is force a re-render by updating its state everytime onClick gets executed. The issue is, we don’t necessarily want RenderCounter to re-render every time its parent re-renders. Since the onClick function passed to RenderCounter gets created on every render, meaning it’s never the same reference, it will always cause React.memo to think that the props have changed, which in turn will always result in a re-render of RenderCounter.
To make sure that this does not happen, we can use useCallback() and ensure that the function we pass to onClick will retain the same reference and prevent RenderCounter from re-rendering unnecessarily.
Closing Remarks
Performance in web applications is more critical than ever as applications grow in size and complexity with each passing moment. This is especially true for real time financial applications where the seemingly chaotic nature of market movements and streamed prices are to be captured and molded into highly responsive and user friendly components. In these applications, every millisecond counts, especially when executing trades and participating in auctions. Such requirements lead us to utilize every performance optimization technique at our disposal. Using frameworks such as React offers inherent performance boosts along with a set of tools that are readily available and can be used to reduce renders and further increase the performance of our applications. However, these tools should be used with caution and only after performance issues have been noticed. Premature optimization could simply lead to more code needing to be unnecessarily executed only to obtain very negligible improvements in performance, or at worst, a decrease in performance. It’s best to measure and attempt to improve performance when issues arise, or if you’re certain a process will have a noticeable impact on your app's performance.
Santiago Gomez
Web Applications Developer,
Adaptive Financial Consulting