Introducing React Compiler #5
Pinned
poteto
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
React Compiler is a new build-time tool that automatically optimizes your React app to improve its performance, particularly on updates (re-renders). The compiler is designed to work with existing JavaScript (and TypeScript) and understands the Rules of React. If your app follows those rules you generally won't need to rewrite any code to use it. We've been testing the compiler at Meta and it's already powering a few production surfaces like instagram.com.
In order to optimize applications, React Compiler automatically memoizes your code. You may be familiar today with memoization through APIs such as
useMemo
,useCallback
, andReact.memo
. With these APIs you can tell React that certain parts of your application don't need to recompute if their inputs haven't changed, reducing work on updates. While powerful, it's easy to forget to apply memoization or apply them incorrectly. This can lead to inefficient updates as React has to check parts of your UI that don't have any meaningful changes.The compiler uses its knowledge of JavaScript and the Rules of React to automatically memoize values or groups of values within your components and hooks. If it detects breakages of the rules, it will automatically skip over just those components or hooks, and continue safely compiling other code.
If your codebase is already very well-memoized, you might not expect to see major performance improvements with the compiler. However, in practice memoizing the correct dependencies that cause performance issues is tricky to get right by hand.
While the compiler is mostly decoupled from Babel, it is currently the primary integration we support for the short-term. Most of the popular React frameworks do have Babel support as fallbacks and so React Compiler can still be used with them. Please refer to the docs for an installation guide. We are looking into other integrations as well and will share more when we have made progress.
What does the compiler do?
It's helpful to first understand the main use cases of memoization in React today:
<Parent />
causes many components in its component tree to re-render, even though only<Parent />
has changedexpensivelyProcessAReallyLargeArrayOfObjects()
inside of your component or hook that needs that data===
on re-rendering so as to prevent an infinite loop in a hook such asuseEffect()
The initial release of React Compiler is primarily focused on improving update performance (i.e., re-rendering existing components), so it primarily focuses on the first two use cases.
Optimizing Re-renders
React lets developers express their UI as a function of their current state (more concretely: their props, state, and context). In its current implementation, when a component's state changes, React will re-render that component and all of its children — unless the developer has applied some form of manual memoization with
useMemo()
,useCallback()
, orReact.memo()
. For example, in the following example,<MessageButton>
will re-render whenever<FriendList>
's state changes:See this example in the React Compiler Playground
React Compiler automatically applies the equivalent of manual memoization, ensuring that only the relevant parts of an app re-render as state changes, which is sometimes referred to as "fine-grained reactivity". In the above example, React Compiler determines that the return value of
<FriendListCard />
can be reused even asfriends
changes, and can avoid recreating this JSX and avoid re-rendering<MessageButton>
as the count changes.Expensive calculations also get memoized
The compiler can also automatically memoize for the second use case:
However, if
expensivelyProcessAReallyLargeArrayOfObjects
is truly an expensive function, you may want to consider implementing its own memoization outside of React, because:So if
expensivelyProcessAReallyLargeArrayOfObjects
was used in many different components, even if the same exact items were passed down, that expensive calculation would be run repeatedly. We recommend profiling first to see if it really is that expensive before making code more complicated.Still an open area of research: Memoization for effects
One open issue that we've seen in real-world apps is that the compiler can sometimes memoize differently from how the code was originally memoized. Typically, this is good because the compiler can memoize more granularly than can be done manually. For example, the compiler memoizes using low-level primitives, not via
useMemo
, so it doesn't need to follow the Rules of Hooks when it comes to generating memoized code while it can still memoize hook calls safely.However, this can cause problems when something that was memoized in a certain way before is no longer memoized exactly the same. The most common example of this is effects —
useEffect
,useLayoutEffect
, etc. — that rely on dependencies not changing in order to prevent an infinite loop or under/over-firing. If you notice any unexpected behavior from effects in your app while trying out the compiler, please file an issue.This is still an open area of research for the React team on the best way to solve this problem. Currently, the compiler will statically validate that auto-memoization matches up with any existing manual memoization. If it can't prove that they're the same, the component or hook is safely skipped over.
For this reason, we recommend keeping any existing
useMemo()
oruseCallback()
calls to ensure that you don't change effect behavior. React Compiler will still attempt to produce more optimal memoization code, but will skip compiling if it can't preserve the original memoization behavior. Instead of removinguseMemo
/useCallback
, we recommend writing new code without relying onuseMemo
anduseCallback
at all.What does the compiler assume?
React Compiler assumes that your code:
strictNullChecks
if using TypeScript), i.e.,if (object.nullableProperty) { object.nullableProperty.foo }
or with optional-chainingobject.nullableProperty?.foo
React Compiler can verify many of the Rules of React statically, and will skip compilation when it detects an error. To see the errors we recommend also installing eslint-plugin-react-compiler.
The compiler found errors in my code!
Please see #8 for a guide on how to successfully rollout the compiler in your codebase.
What can the compiler "see"?
Currently, React Compiler operates on a single file at a time, meaning that it only uses the information inside of a single file in order to perform its optimizations. While this may at first glance seem limited, in practice we've found that because of React's programming model of using plain JavaScript values and compiler-friendly conventions and rules, this approach works surprisingly well. Of course, there are tradeoffs like not being able to use information from another file, which would allow even more fine-grained memoization. But the current single file approach balances the complexity of the compiler against output quality, and our results show it's a good tradeoff so far.
While the compiler does not currently use type information from typed JavaScript languages like TypeScript or Flow, internally it has its own type system that helps it understand your code better.
[1] A compiler term that means an expression which is a part of a larger expression. For example
useState(makeInitialState())
is made up of 2 expressions: the CallExpressionuseState(...)
and CallExpressionmakeInitialState
. The nested CallExpression is a sub-expression of the CallExpression.Beta Was this translation helpful? Give feedback.
All reactions