Hey folks, this is my next article in the series based on interviews I had given recently. In this one, I'll break down how you can improve the performance of your React app by having a control over your renders, and the benefits of doing so.
Why do we care about an app's performance? Performance is a major factor into contributing to how long a user will stick with your app. There have been several studies showcasing drop-off in users based on how the app runs. We don't even need studies for this, just think back upon your personal experiences and the apps that you use.
Why do we care about stickiness in an app? The longer a user sticks with your app, the more likely it's that they'll continue to use it, and hence, spend money on it.
Solving Performance in a webapp
The main ways to solve performance in a web-app are:
- Network Optimization - Optimize your app to fetch assets faster over the wire via optimizing images, compressing code, etc.
- Render Optimization - Optimize to reduce the number of DOM updates and CSS reflows.
- JS Optimization- Various optimization to reduce the number of JS that runs in the app.
It provides a declarative API for render optimization. Additionally, it helps in render optimization by automagically batching DOM updates for us. Generally, the solutions you can implement to optimize network calls and JS are transferrable outside of React.
Anatomy of a React Render
I've talked about this previously here, along with why useState and useEffect exist. In short, React renders in 2 phases:
- The Render Phase - Which converts your JSX code into an object (popularly known as VDOM) which React can parse.
- The Commit Phase - Which keeps checking the rendered object for updates, and batch-commits them to the DOM.
Why do useState and useEffect Exist?
In short, to create an explicit dependency between your state and the side-effects that its update should trigger. To keep these dependencies consistent, and to avoid unexpected behavior, it's recommended to pass ALL dependencies to the dependency array of a
useCallback, or any other hook which expects a dependency array to be passed to it.
What does the
key prop in React do?
React holds a reference to the DOM Node structure in each element's Fiber Node, Fiber being the current implementation of React's reconciliation algorithm. Because of this, a developer generally doesn't have to worry about providing unique references for each DOM Node to React.
However, in structures like list, the order of React elements might change arbitrarily. To maintain a one-one mapping for the order of DOM Nodes inside of React and the browser, it's recommended to pass a
key prop which uniquely identifies a React element inside of a list.
key being passed to DOM Nodes in a list is not unique, your app may start breaking in unexpected ways. Additionally, since React uses the
key prop to uniquely identify nodes, changing this prop results in a remount of the component, resetting all internal state and rerunning all effects.
To see me build a list and slowly start breaking it, watch the talk from this point.
In programming, memoization is the concept of storing (or remembering) the result of a function, for a given set of input parameters. Memoization can only be applied to pure functions, ie, a function which always returns the same output for the same set of input parameters.
Since React components and hooks are functions, they can benefit from memoization as well. Hence, React provides us with a useful set of memoization tools:
- useCallback - for stabilizing the reference to a function callback between renders, with a dependency array used for memoization.
- useMemo - for stabilizing the reference to a value between renders, with a dependency array used for memoization.
- React.memo - A higher-order-component (HOC) provided by React to wrap your components in. It stabilizes the reference to a component, automatically using the props passed to the component as a dependency array.
Since React components and the values inside them aren't stabilized by default, updating state in React can lead to unexpected behavior. For eg, updating a state variable in a component via
setState rerenders all of its children components as well.
One can use React.memo from the previous example to stabilize child components, and hence, prevent unneccessary renders on parent state update.
Sometimes one only needs to store a reference to a state, but not react to changes to that variable. For eg, if you're tracking a user's mouse movements, it would be unrealistic to expect your app to rerender for every unique
x,y position that a user's mouse is present in. In this case, it makes more sense to use the useRef hook to store a reference to the user's mouse position.
useContext and State Management
useContext is a hook which creates a component-level context in its Provider, which passes down its state to all child components. The only benefit of using this hook is to avoid props-drilling and access this state from all child components.
However, useContext comes with a major disadvantage which prevents it from being a viable state-management solution. All children of the
Context.Provider rerender when the parent component rerenders, as we saw in the previous State Cascades section.
We can of course, use
React.memo to prevent rerenders here, but, there's a catch. Every component that tries to access any state from the context, will rerender, whenever any other part of the context updates. This part needs a visual example to understand what's happening, so I recommend checking the video out from 35:52 to understand what happens here.
Instead, you should use a proper state-management library built on top of
useContext, like zustand, which you can check out in the complete video below.