The most efficient way to identify performance issues in our web application is... just using the web platform itself. Trying to analyse every facet of the platform in search of inefficient renderings can be a time-consuming task, and addressing these less critical issues can consume even more time. Therefore, it is crucial to prioritize tackling real problems that significantly impact performance to ensure our efforts are efficiently utilized.
These critical performance problems will appear in a natural way during the application's production usage. It's in this context that you may observe certain actions taking more time than expected. These issues may also arise during the development process, and it is in this case when React Dev Tools, integrated into your browser, become invaluable. It can assist you in pinpointing the origin cause of the problem, facilitating an effective resolution process.
We are going to use the React Developers Tools, and you can download the Chrome extension at this link
To be able to use the Profiler tool the application needs to be running in production mode, so first we have to run a build command with the --profile flag
npm run build -- --profile
After that we can serve the build with
serve -s build
Using profiler to find a performance issue
Having our application served on production mode we can make use of the Chrome dev tools properly. We can open the Chrome console with cmd+opt+j and go to the "profile" tab
In normal use, when you are ready to start recording the performance behaviour on your platform, you can click on the "Start profiling" button and then start making actions.
Finally, when clicking on the "Stop profiling" button we´ll see the result of the profiling with the identifier of each element.
And clicking on each, an explanation of what has triggered the re-render of each react element.
We can feel that something is running slow on our platform. If we see the profiler after the recording, we can check that each keypress an input takes 150ms
We can check on the performance tab a piece of more detailed information, which lets us know that something is happening on a function call triggered on each keydown.
After reviewing the code, we can find that there is an expensive function being called on each render.
We´re passing a direct value as an argument to the useState
, which is used as the initial state during every render. This means that the generateRandomColor function is called on every render, potentially leading to unnecessary computations if the function is not memorized or if it performs expensive operations.
In this case, using the function as an argument (() => generateRandomColor()) can be more efficient because it ensures that the initialization is performed only once.
After changing the line 14 to
const [correctAnswer, setCorrectAnswer] = useState(() => generateRandomColor());
We are passing a function as an argument to useState, in this case React treats it as a lazy initializer. This means that the function is only executed once, during the initial render, to determine the initial state value. This can be beneficial if the initialization is computationally expensive or involves asynchronous operations.
After applying those changes to the code and running the performance tab, we can see how the expensive function that was running on each keydown is not there anymore.
Pushing state changes down to solve performance issues
In another example, after running the profiler we can find that there is a component with the suspicious name of "expensive" being rendered on each keypress
After taking a look at the code, we can find that this component is on the app at the same level that some useState hooks, meaning that on each state change, all the react trees of components on this level are going to be re-rendered.
The expensive component is not receiving prop at all, so we can extract all the mutable logic and put it into another component called game.jsx
In this way, we don´t need to touch the expensiveComponent.jsx component, it may contain heavy logic and it´s not needed to refactor it, maybe it´s not even possible. So, putting the logic in this component, we are pushing down the rerenders on the react tree, separating the problem and the changes of the new game.jsx component will not affect the expensive one.
In conclusion, while React Dev Tools offer valuable insights during the development process, particularly in identifying performance bottlenecks, it's important to prioritize issues according to the attention required. By leveraging tools like the Profiler in React Dev Tools, we can detect specific areas of concern and optimize the code according to it. Strategic architectural decisions, such as pushing state changes down the component tree, can easily alleviate unnecessary re-renders and enhance overall application performance.
Thanks for reading! I hope you can use these tips to improve the performance of your application through a combination of tooling, thoughtful design, and focused optimizations.
Top comments (0)