paint-brush
Improving React App Performance: Leveraging Browser APIs to help React Compilerby@toli444
144 reads

Improving React App Performance: Leveraging Browser APIs to help React Compiler

by Anatoli SemianiakaSeptember 23rd, 2024
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

React Compiler can't do everything, so we need to work around its limitations. We can improve our React app code by using built-in browser APIs. In this article, we’ll also look at browser APIs that can help optimize React apps' performance in areas beyond React Compiler's scope.
featured image - Improving React App Performance: Leveraging Browser APIs to help React Compiler
Anatoli Semianiaka HackerNoon profile picture

In the React Compiler demo talk, one of the code examples was an event handler that contains a React state that depends on whether an HTML element is in focus. In my opinion, that example doesn’t demonstrate the power of React Compiler but instead shows its limitations. After reading this article, you’ll see why I think so.


We’ll look at how we can improve our React app code by using built-in browser APIs and help React Compiler perform better, working around its limitations. We’ll also look at browser APIs that can help optimize React apps' performance in areas beyond React Compiler's scope.


document.activeElement — to check if an element is in focus

To check if an element is in focus, instead of creating a React state, we can use document.activeElement which returns an element currently in focus. We can read its ID and compare it with an ID of our element.


Usage of document.activeElement to check if an element is in focus.


:hover, :focus, :focus-within — for handling hover, focus, and other states

Sometimes we dive into React and JS too much and use React state to handle hover or focus states. That actually results in unnecessary re-renders and code complexity. This happens especially often if the project uses Tailwind CSS or Styled-components libraries. We need to remember the power of other technologies available to us, such as CSS. The best way to handle hover, focus, and other states is to use corresponding CSS pseudo-classes.


Usage of :hover pseudo class for styling child based on parent’s hover state.


  • Handling Hover, Focus, and other states in Tailwind CSS should be done using special modifiers.
  • For styling based on parent state Tailwind CSS has a special group-{modifier} class.
  • For styling based on if any of the child elements are focused, there’s a focus-within within pseudo-class in CSS.
  • Styled-components support the same syntax as CSS for handling hover, focus, and other states.

<details />, <select />, <input />, <form /> — to get needed functionality out of the box

With native HTML elements, we can get a lot of functionality out of the box with great performance and accessibility, without the need to write extra code or add extra state in React.


<details /> — for collapsible sections

Open/Close states are handled automatically by the browser. We can use CSS to style it.


Using the <details /> element to create a collapsible section.


<select /> — for dropdowns

Open/Close states are handled automatically by the browser. The dropdown menu is optimized and responsive even on large lists of options, it is convenient to use on both desktop and mobile devices and it is accessible out of the box.


Using the <select /> element to create a dropdown.


<input /> and <form /> — for forms and submitting information

Form elements keep an internal state without the need to create a state in React. Of course, for some scenarios, we can get more functionality by using some third-party form libraries in React, but we shouldn’t underestimate how much we can get out of the box with native HTML elements. Check out this example on MDN with a fully functioning payment form created using just plain HTML. Also, check out my other article for another example of how HTML forms can be used to minimize re-renders in React.

CustomEvent — for decoupled communication between components

Sometimes we need our components to communicate with each other independently, without us having to create a React.Context somewhere at the top of the application. We may need this if we are creating a component library, or if some of our components are web-components. For this purpose, we can use CustomEvents. Here’s an example of using a CustomEvent to implement a tooltip that closes when another one opens.


Using a CustomEvent to implement a tooltip that closes when another tooltip is opened.


This it how it looks in the browser:


Demo of a tooltip that closes when another tooltip is opened.


Only opening/closing tooltips re-render, in comparison to React.Context which when changed causes every component subscribed to it to be re-rendered.

CSS transition and transform — for performant animations

The transition property in CSS allows us to create animations. We should create CSS animations only using transform properties (scale, translate etc.) and not using positioning properties (top/left/height/width) which will cause continuous layout computation.


Before using any CSS property for animation (other than transform and opacity), we need to determine the property's impact on the rendering pipeline. We should avoid any property that triggers layout or paint unless absolutely necessary.


Browsers give us out-of-the-box functionality for creating animations and can optimize rendering flows if correct properties are used. We should always try to create our animations using CSS transitions/animations where possible.

requestAnimationFrame, requestIdleCallback — for non-blocking computations

If we need to change the CSS properties of an element using JavaScript we should do it with requestAnimationFrame API which will help in not blocking the user’s other operation but executing it at the end of the frame.


Long-running and low-priority JavaScript tasks should be split into smaller ones and executed inside requestIdleCallback. There’s an “Idle Until Urgent” pattern.

Web Workers — for multi-threading

Web Workers make it possible to run a script operation in a background thread separate from the main execution thread (in this way they are different from requestAnimationFrame, requestIdleCallback that a running in the main thread). The advantage of this is that laborious processing can be performed in a separate thread, allowing the main thread to run without being blocked/slowed down.


One limitation of Web workers is that they can’t directly access the DOM. But they can communicate with the window context through a messaging pipeline, meaning that a web worker can indirectly access the DOM in a way.


One of the examples of using Web Workers in React apps can be a financial dashboard that processes large datasets to generate complex reports and charts. From the React app, we can send a message to the web worker to start the calculation. Once the worker is done, it sends the result back to the main thread (our React app) where we can use it to update the UI (set React state).

Conclusion

The example that I mentioned at the beginning of the article was used to demonstrate how React Compiler applies memoization to optimize re-renders when the state changes. But in that example, the state was not needed, and the same functionality could be achieved by just using built-in browser APIs. Unfortunately, React Compiler does not know about it, and it has to process the code that was given as is. If the code contains a redundant state, it will stay there. When the state changes, React Compiler will minimize (not guaranteed) how much React re-renders and memoization-related computations will still occur.


So, the main principle of structuring state in React: avoiding redundant state, is still worth considering. Also, the more states, the more dependencies in the code that need to be kept in sync and covered by tests.

Summary

  • Rely on the browser’s built-in APIs as much as possible, as they are available “for free” (they don’t increase bundle size, etc.) and are almost always the best optimized.
  • Try to avoid creating states in React for states that can be handled by the browser.
  • React Compiler will not be able to cover all optimizations of your application. Many of them are beyond its scope. But many optimizations can be achieved using built-in browser APIs.
  • Before installing a new project dependency, double-check if it is really needed. With built-in browser APIs, we can get optimized performance out of the box.


👏 Thanks for reading!