Skip to content

Conversation

stipsan
Copy link
Member

@stipsan stipsan commented Oct 29, 2024

Now that the React Compiler is in Beta, and fully supports targeting React 18 (and above), we can start leveraging it to boost the performance, and reduce the memory footpring, of our libraries 🎉

I've tested this build [email protected] on the Studio and tests are passing 🥳
It doesn't seem like the eFPS testing suite is setup to capture the performance benefits atm, compare a regular bump with this one and the differences are so small I think they're inconclusive.

It's really cool to look at how the compiler is optimizing the code, how granular it is.

For example it even hoists out inline functions when it can, like this:

-map((value) => ({ snapshot: value, error: void 0 }))
+map(_temp$1)
+function _temp$1(value) {
+  return {
+    snapshot: value,
+    error: void 0
+  };
+}
-catchError((error) => of({ snapshot: void 0, error }))
+catchError(_temp2)
+function _temp2(error) {
+  return of({
+    snapshot: void 0,
+    error
+  });
+}
-share({ resetOnRefCountZero: () => timer(0, asapScheduler) })
+share({resetOnRefCountZero: _temp4})
+function _temp4() {
+  return timer(0, asapScheduler);
+}

And finally, note how it removes the useMemo entirely, and replaces it with granular, low level API:

import {c} from 'react-compiler-runtime'

Which knows ahead of time exactly how much cache it'll need:

const $ = c(9)

And here's how that's used to ensure that cache.get(observable) is only called when observable has changed:

-const instance = cache.get(observable);
+let t0;
+$[0] !== observable ? (t0 = cache.get(observable), $[0] = observable, $[1] = t0) : t0 = $[1];
+const instance = t0;

Copy link

vercel bot commented Oct 29, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
react-rx ✅ Ready (Inspect) Visit Preview 💬 Add feedback Oct 31, 2024 1:51pm

Copy link

socket-security bot commented Oct 29, 2024

New dependencies detected. Learn more about Socket for GitHub ↗︎

Package New capabilities Transitives Size Publisher
npm/@vitejs/[email protected] Transitive: environment, filesystem, network, shell +54 17.9 MB vitebot
npm/[email protected] Transitive: environment +3 7.73 MB react-bot
npm/[email protected] None 0 72.2 kB react-bot

View full report↗︎

Comment on lines 84 to 89
() => {
const instance = cache.get(observable)!
if (instance.error) {
throw instance.error
}
return instance.snapshot
},
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only the subscribe argument in useSyncExternalStore needs to be memoized, as it'll run teardown + setup every time it changes. The getSnapshot and getServerSnapshot arguments don't have to be memoized and the React Compiler uses less memory if we only memoize subscribe 🙌

@stipsan stipsan force-pushed the enable-react-compiler branch from 26341f9 to 9483f6e Compare October 31, 2024 13:50
@stipsan stipsan requested a review from bjoerge October 31, 2024 13:52
@stipsan stipsan marked this pull request as ready for review October 31, 2024 13:52
@@ -1,6 +1,6 @@
{
"name": "react-rx",
"version": "4.0.1",
"version": "4.1.0-canary.5",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is ignored by semantic-release

@@ -75,6 +75,7 @@
"prettier": "@sanity/prettier-config",
"dependencies": {
"observable-callback": "^1.0.3",
"react-compiler-runtime": "19.0.0-beta-6fc168f-20241025",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The guidance atm is to use exact versions, generated like:

pnpm add react-compiler-runtime@beta --save-exact

@@ -106,7 +109,7 @@
"vitest": "^2.1.4"
},
"peerDependencies": {
"react": "^18.3 || >=19.0.0-rc",
"react": "^18.3 || >=19.0.0-0",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

19.0.0-0 is more permissive than 19.0.0-rc, and for example permits the canary versions of 19 that are used on canary builds of next.

},
}
}, [observable])
const subscribe = useCallback(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rewrite to useCallback on just subscribe, instead of keeping the useMemo, is to avoid agressive subscribe + unsubscribe loops.

The Compiler looks at what is actually used inside the useMemo, which is instance.observable, instance.snapshot and instance.error.
In a sense, it's as if it rewrote our code to be:

const instance = useMemo(() => cache.get(observable), [observable])
const store = useMemo(() => {
  // ...
}, [instance.observable, instance.snapshot, instance.error])

This is causing store to change identity every time snapshot changes.

So by splitting them up, to use useCallback instead, this is what happens instead (conceptually):

const instance = useMemo(() => cache.get(observable), [observable])
const subscribe = useCallback(() => {
  // ...
}, [instance.observable])

And now we avoid the problem, as instance.observable is stable. useSyncExternalStore doesn't care if getSnapshot or getServerSnapshot changes identity between renders, it only requires subscribe to be memoized correctly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, this is great - thanks for the explainer!

Comment on lines +5 to +9
plugins: [
react({
babel: {plugins: [['babel-plugin-react-compiler', {target: '18'}]]},
}),
],
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's vital that the test suite is also using the react compiler, to ensure we're testing the behaviour of the code we're shipping, not the unoptimised code.
Especially since our testing suite is testing implementation details around memoization.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

Copy link
Member

@bjoerge bjoerge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing stuff! LGTM ✨

@stipsan stipsan disabled auto-merge October 31, 2024 14:30
@stipsan stipsan merged commit e345f8c into current Oct 31, 2024
12 checks passed
@stipsan stipsan deleted the enable-react-compiler branch October 31, 2024 14:30
@stipsan stipsan restored the enable-react-compiler branch November 4, 2024 15:48
@stipsan stipsan deleted the enable-react-compiler branch January 13, 2025 10:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants