Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Experiment] Reuse memo cache after interruption #28878

Merged
merged 1 commit into from
Apr 19, 2024

Conversation

acdlite
Copy link
Collaborator

@acdlite acdlite commented Apr 19, 2024

Adds an experimental feature flag to the implementation of useMemoCache, the internal cache used by the React Compiler (Forget).

When enabled, instead of treating the cache as copy-on-write, like we do with fibers, we share the same cache instance across all render attempts, even if the component is interrupted before it commits.

If an update is interrupted, either because it suspended or because of another update, we can reuse the memoized computations from the previous attempt. We can do this because the React Compiler performs atomic writes to the memo cache, i.e. it will not record the inputs to a memoization without also recording its output.

This gives us a form of "resuming" within components and hooks.

This only works when updating a component that already mounted. It has no impact during initial render, because the memo cache is stored on the fiber, and since we have not implemented resuming for fibers, it's always a fresh memo cache, anyway.

However, this alone is pretty useful — it happens whenever you update the UI with fresh data after a mutation/action, which is extremely common in a Suspense-driven (e.g. RSC or Relay) app.

So the impact of this feature is faster data mutations/actions (when the React Compiler is used).

Adds an experimental feature flag to the implementation of useMemoCache,
the internal cache used by the React Compiler (Forget).

When enabled, instead of treating the cache as copy-on-write, like we
do with fibers, we share the same cache instance across all render
attempts, even if the component is interrupted before it commits.

If an update is interrupted, either because it suspended or because of
another update, we can reuse the memoized computations from the previous
attempt. We can do this because the React Compiler performs atomic
writes to the memo cache, i.e. it will not record the inputs to a
memoization without also recording its output.

This gives us a form of "resuming" within components and hooks.

This only works when updating a component that already mounted. It has
no impact during initial render, because the memo cache is stored on the
fiber, and since we have not implemented resuming for fibers, it's
always a fresh memo cache, anyway.

However, this alone is pretty useful — it happens whenever you update
the UI with fresh data after a mutation/action, which is extremely
common in a Suspense-driven (e.g. RSC or Relay) app.

So the impact of this feature is faster data mutations/actions.
@facebook-github-bot facebook-github-bot added CLA Signed React Core Team Opened by a member of the React Core Team labels Apr 19, 2024
@react-sizebot
Copy link

Comparing: f5ce642...eb8b3e8

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.min.js = 168.92 kB 168.92 kB = 52.94 kB 52.94 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js = 170.57 kB 170.57 kB = 53.44 kB 53.44 kB
facebook-www/ReactDOM-prod.classic.js +0.03% 590.83 kB 590.99 kB +0.03% 103.91 kB 103.94 kB
facebook-www/ReactDOM-prod.modern.js +0.03% 566.65 kB 566.80 kB +0.04% 100.10 kB 100.14 kB
test_utils/ReactAllWarnings.js Deleted 64.44 kB 0.00 kB Deleted 16.10 kB 0.00 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
oss-experimental/react-art/cjs/react-art.production.js +0.25% 587.85 kB 589.31 kB +0.34% 131.63 kB 132.08 kB
oss-stable-semver/react-test-renderer/cjs/react-test-renderer.production.js +0.25% 589.55 kB 591.01 kB +0.34% 132.24 kB 132.69 kB
oss-stable/react-test-renderer/cjs/react-test-renderer.production.js +0.25% 589.58 kB 591.03 kB +0.34% 132.27 kB 132.72 kB
oss-experimental/react-test-renderer/cjs/react-test-renderer.production.js +0.25% 590.98 kB 592.44 kB +0.34% 132.60 kB 133.05 kB
oss-experimental/react-reconciler/cjs/react-reconciler.production.js +0.22% 676.32 kB 677.78 kB +0.31% 148.61 kB 149.07 kB
oss-experimental/react-reconciler/cjs/react-reconciler.profiling.js +0.20% 716.34 kB 717.80 kB +0.29% 155.92 kB 156.36 kB
test_utils/ReactAllWarnings.js Deleted 64.44 kB 0.00 kB Deleted 16.10 kB 0.00 kB

Generated by 🚫 dangerJS against eb8b3e8

@josephsavona
Copy link
Contributor

Awesome! My intuition is that this is overall better, and it certainly is an improvement in the test case. The main concern would be a situation where the memo slots that are being updated in the low and high pri updates overlap. So the low-priority update starts and clears out some values but suspends partway, then the high-pri update happens and has to redo some work (that would have been memoized still, w the feature flag off), then the low-pri update resumes and has to redo that work again (since the high-pri update just undid it).

Again, my intuition is that this scenario is rare enough that this is overall a win, just wanted to flag it. It makes sense to test this, though we have enough things up in the air right now that it might not be the best time. We'll chat in our internal sync next week and follow-up with you.

@acdlite
Copy link
Collaborator Author

acdlite commented Apr 19, 2024

The main concern would be a situation where the memo slots that are being updated in the low and high pri updates overlap.

Yeah I remember you and I discussed this at some point, maybe even in the original useMemoCache PR.

Since it's rare for high pri updates to "switch back" to the current state (compared to the scenario in the test case I added), I agree this is the better trade off. (Also, comparing only against the current UI values is already a form of this trade off, since theoretically we could cache every previous set of inputs we ever received.)

@acdlite acdlite merged commit ea26e38 into facebook:main Apr 19, 2024
38 checks passed
github-actions bot pushed a commit that referenced this pull request Apr 19, 2024
Adds an experimental feature flag to the implementation of useMemoCache,
the internal cache used by the React Compiler (Forget).

When enabled, instead of treating the cache as copy-on-write, like we do
with fibers, we share the same cache instance across all render
attempts, even if the component is interrupted before it commits.

If an update is interrupted, either because it suspended or because of
another update, we can reuse the memoized computations from the previous
attempt. We can do this because the React Compiler performs atomic
writes to the memo cache, i.e. it will not record the inputs to a
memoization without also recording its output.

This gives us a form of "resuming" within components and hooks.

This only works when updating a component that already mounted. It has
no impact during initial render, because the memo cache is stored on the
fiber, and since we have not implemented resuming for fibers, it's
always a fresh memo cache, anyway.

However, this alone is pretty useful — it happens whenever you update
the UI with fresh data after a mutation/action, which is extremely
common in a Suspense-driven (e.g. RSC or Relay) app.

So the impact of this feature is faster data mutations/actions (when the
React Compiler is used).

DiffTrain build for commit ea26e38.
github-actions bot pushed a commit that referenced this pull request Apr 19, 2024
Adds an experimental feature flag to the implementation of useMemoCache,
the internal cache used by the React Compiler (Forget).

When enabled, instead of treating the cache as copy-on-write, like we do
with fibers, we share the same cache instance across all render
attempts, even if the component is interrupted before it commits.

If an update is interrupted, either because it suspended or because of
another update, we can reuse the memoized computations from the previous
attempt. We can do this because the React Compiler performs atomic
writes to the memo cache, i.e. it will not record the inputs to a
memoization without also recording its output.

This gives us a form of "resuming" within components and hooks.

This only works when updating a component that already mounted. It has
no impact during initial render, because the memo cache is stored on the
fiber, and since we have not implemented resuming for fibers, it's
always a fresh memo cache, anyway.

However, this alone is pretty useful — it happens whenever you update
the UI with fresh data after a mutation/action, which is extremely
common in a Suspense-driven (e.g. RSC or Relay) app.

So the impact of this feature is faster data mutations/actions (when the
React Compiler is used).

DiffTrain build for [ea26e38](ea26e38)
bigfootjon pushed a commit that referenced this pull request Apr 25, 2024
Adds an experimental feature flag to the implementation of useMemoCache,
the internal cache used by the React Compiler (Forget).

When enabled, instead of treating the cache as copy-on-write, like we do
with fibers, we share the same cache instance across all render
attempts, even if the component is interrupted before it commits.

If an update is interrupted, either because it suspended or because of
another update, we can reuse the memoized computations from the previous
attempt. We can do this because the React Compiler performs atomic
writes to the memo cache, i.e. it will not record the inputs to a
memoization without also recording its output.

This gives us a form of "resuming" within components and hooks.

This only works when updating a component that already mounted. It has
no impact during initial render, because the memo cache is stored on the
fiber, and since we have not implemented resuming for fibers, it's
always a fresh memo cache, anyway.

However, this alone is pretty useful — it happens whenever you update
the UI with fresh data after a mutation/action, which is extremely
common in a Suspense-driven (e.g. RSC or Relay) app.

So the impact of this feature is faster data mutations/actions (when the
React Compiler is used).

DiffTrain build for commit ea26e38.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed React Core Team Opened by a member of the React Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants