Skip to content

Commit 2b01d44

Browse files
committed
[Experiment] Reuse memo cache after interruption (#28878)
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)
1 parent a6449bd commit 2b01d44

28 files changed

+371
-96
lines changed

compiled/facebook-www/JSXDEVRuntime-dev.classic.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ var enableDebugTracing = dynamicFeatureFlags.enableDebugTracing,
100100
enableTransitionTracing = dynamicFeatureFlags.enableTransitionTracing,
101101
enableRenderableContext = dynamicFeatureFlags.enableRenderableContext,
102102
enableRefAsProp = dynamicFeatureFlags.enableRefAsProp,
103-
disableDefaultPropsExceptForClasses = dynamicFeatureFlags.disableDefaultPropsExceptForClasses; // On WWW, false is used for a new modern build.
103+
disableDefaultPropsExceptForClasses = dynamicFeatureFlags.disableDefaultPropsExceptForClasses;
104+
// On WWW, false is used for a new modern build.
104105
// because JSX is an extremely hot path.
105106

106107
var disableStringRefs = false;

compiled/facebook-www/JSXDEVRuntime-dev.modern.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ var enableDebugTracing = dynamicFeatureFlags.enableDebugTracing,
100100
enableTransitionTracing = dynamicFeatureFlags.enableTransitionTracing,
101101
enableRenderableContext = dynamicFeatureFlags.enableRenderableContext,
102102
enableRefAsProp = dynamicFeatureFlags.enableRefAsProp,
103-
disableDefaultPropsExceptForClasses = dynamicFeatureFlags.disableDefaultPropsExceptForClasses; // On WWW, true is used for a new modern build.
103+
disableDefaultPropsExceptForClasses = dynamicFeatureFlags.disableDefaultPropsExceptForClasses;
104+
// On WWW, true is used for a new modern build.
104105
// because JSX is an extremely hot path.
105106

106107
var disableStringRefs = false;

compiled/facebook-www/REVISION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
446aa9a632670eb9a373d897309452e24c10c55e
1+
ea26e38e33bffeba1ecc42688d7e8cd7e0da1c02

compiled/facebook-www/React-dev.classic.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ if (
2525
) {
2626
__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error());
2727
}
28-
var ReactVersion = '19.0.0-www-classic-a296fa2c';
28+
var ReactVersion = '19.0.0-www-classic-8ac39151';
2929

3030
// ATTENTION
3131
// When adding new symbols to this file,
@@ -429,7 +429,8 @@ var enableDebugTracing = dynamicFeatureFlags.enableDebugTracing,
429429
enableTransitionTracing = dynamicFeatureFlags.enableTransitionTracing,
430430
enableRenderableContext = dynamicFeatureFlags.enableRenderableContext,
431431
enableRefAsProp = dynamicFeatureFlags.enableRefAsProp,
432-
disableDefaultPropsExceptForClasses = dynamicFeatureFlags.disableDefaultPropsExceptForClasses; // On WWW, false is used for a new modern build.
432+
disableDefaultPropsExceptForClasses = dynamicFeatureFlags.disableDefaultPropsExceptForClasses;
433+
// On WWW, false is used for a new modern build.
433434
// because JSX is an extremely hot path.
434435

435436
var disableStringRefs = false;

compiled/facebook-www/React-dev.modern.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ if (
2525
) {
2626
__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error());
2727
}
28-
var ReactVersion = '19.0.0-www-modern-fe830ed2';
28+
var ReactVersion = '19.0.0-www-modern-f6026193';
2929

3030
// ATTENTION
3131
// When adding new symbols to this file,
@@ -429,7 +429,8 @@ var enableDebugTracing = dynamicFeatureFlags.enableDebugTracing,
429429
enableTransitionTracing = dynamicFeatureFlags.enableTransitionTracing,
430430
enableRenderableContext = dynamicFeatureFlags.enableRenderableContext,
431431
enableRefAsProp = dynamicFeatureFlags.enableRefAsProp,
432-
disableDefaultPropsExceptForClasses = dynamicFeatureFlags.disableDefaultPropsExceptForClasses; // On WWW, true is used for a new modern build.
432+
disableDefaultPropsExceptForClasses = dynamicFeatureFlags.disableDefaultPropsExceptForClasses;
433+
// On WWW, true is used for a new modern build.
433434
// because JSX is an extremely hot path.
434435

435436
var disableStringRefs = false;

compiled/facebook-www/ReactART-dev.classic.js

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ function _assertThisInitialized(self) {
6363
return self;
6464
}
6565

66-
var ReactVersion = '19.0.0-www-classic-94931f64';
66+
var ReactVersion = '19.0.0-www-classic-d14dfff2';
6767

6868
var LegacyRoot = 0;
6969
var ConcurrentRoot = 1;
@@ -161,7 +161,8 @@ var enableDebugTracing = dynamicFeatureFlags.enableDebugTracing,
161161
enableInfiniteRenderLoopDetection = dynamicFeatureFlags.enableInfiniteRenderLoopDetection,
162162
enableRenderableContext = dynamicFeatureFlags.enableRenderableContext,
163163
enableRefAsProp = dynamicFeatureFlags.enableRefAsProp,
164-
disableDefaultPropsExceptForClasses = dynamicFeatureFlags.disableDefaultPropsExceptForClasses; // On WWW, false is used for a new modern build.
164+
disableDefaultPropsExceptForClasses = dynamicFeatureFlags.disableDefaultPropsExceptForClasses,
165+
enableNoCloningMemoCache = dynamicFeatureFlags.enableNoCloningMemoCache; // On WWW, false is used for a new modern build.
165166
var enableProfilerTimer = true;
166167
var enableProfilerCommitHooks = true;
167168
var enableProfilerNestedUpdatePhase = true;
@@ -8403,7 +8404,30 @@ function useMemoCache(size) {
84038404

84048405
if (currentMemoCache != null) {
84058406
memoCache = {
8406-
data: currentMemoCache.data.map(function (array) {
8407+
// When enableNoCloningMemoCache is enabled, instead of treating the
8408+
// cache as copy-on-write, like we do with fibers, we share the same
8409+
// cache instance across all render attempts, even if the component
8410+
// is interrupted before it commits.
8411+
//
8412+
// If an update is interrupted, either because it suspended or
8413+
// because of another update, we can reuse the memoized computations
8414+
// from the previous attempt. We can do this because the React
8415+
// Compiler performs atomic writes to the memo cache, i.e. it will
8416+
// not record the inputs to a memoization without also recording its
8417+
// output.
8418+
//
8419+
// This gives us a form of "resuming" within components and hooks.
8420+
//
8421+
// This only works when updating a component that already mounted.
8422+
// It has no impact during initial render, because the memo cache is
8423+
// stored on the fiber, and since we have not implemented resuming
8424+
// for fibers, it's always a fresh memo cache, anyway.
8425+
//
8426+
// However, this alone is pretty useful — it happens whenever you
8427+
// update the UI with fresh data after a mutation/action, which is
8428+
// extremely common in a Suspense-driven (e.g. RSC or Relay) app.
8429+
data: enableNoCloningMemoCache ? currentMemoCache.data : // Clone the memo cache before each render (copy-on-write)
8430+
currentMemoCache.data.map(function (array) {
84078431
return array.slice();
84088432
}),
84098433
index: 0

compiled/facebook-www/ReactART-dev.modern.js

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ function _assertThisInitialized(self) {
6363
return self;
6464
}
6565

66-
var ReactVersion = '19.0.0-www-modern-c57fe631';
66+
var ReactVersion = '19.0.0-www-modern-41846fdd';
6767

6868
var LegacyRoot = 0;
6969
var ConcurrentRoot = 1;
@@ -161,7 +161,8 @@ var enableDebugTracing = dynamicFeatureFlags.enableDebugTracing,
161161
enableInfiniteRenderLoopDetection = dynamicFeatureFlags.enableInfiniteRenderLoopDetection,
162162
enableRenderableContext = dynamicFeatureFlags.enableRenderableContext,
163163
enableRefAsProp = dynamicFeatureFlags.enableRefAsProp,
164-
disableDefaultPropsExceptForClasses = dynamicFeatureFlags.disableDefaultPropsExceptForClasses; // On WWW, true is used for a new modern build.
164+
disableDefaultPropsExceptForClasses = dynamicFeatureFlags.disableDefaultPropsExceptForClasses,
165+
enableNoCloningMemoCache = dynamicFeatureFlags.enableNoCloningMemoCache; // On WWW, true is used for a new modern build.
165166
var enableProfilerTimer = true;
166167
var enableProfilerCommitHooks = true;
167168
var enableProfilerNestedUpdatePhase = true;
@@ -8192,7 +8193,30 @@ function useMemoCache(size) {
81928193

81938194
if (currentMemoCache != null) {
81948195
memoCache = {
8195-
data: currentMemoCache.data.map(function (array) {
8196+
// When enableNoCloningMemoCache is enabled, instead of treating the
8197+
// cache as copy-on-write, like we do with fibers, we share the same
8198+
// cache instance across all render attempts, even if the component
8199+
// is interrupted before it commits.
8200+
//
8201+
// If an update is interrupted, either because it suspended or
8202+
// because of another update, we can reuse the memoized computations
8203+
// from the previous attempt. We can do this because the React
8204+
// Compiler performs atomic writes to the memo cache, i.e. it will
8205+
// not record the inputs to a memoization without also recording its
8206+
// output.
8207+
//
8208+
// This gives us a form of "resuming" within components and hooks.
8209+
//
8210+
// This only works when updating a component that already mounted.
8211+
// It has no impact during initial render, because the memo cache is
8212+
// stored on the fiber, and since we have not implemented resuming
8213+
// for fibers, it's always a fresh memo cache, anyway.
8214+
//
8215+
// However, this alone is pretty useful — it happens whenever you
8216+
// update the UI with fresh data after a mutation/action, which is
8217+
// extremely common in a Suspense-driven (e.g. RSC or Relay) app.
8218+
data: enableNoCloningMemoCache ? currentMemoCache.data : // Clone the memo cache before each render (copy-on-write)
8219+
currentMemoCache.data.map(function (array) {
81968220
return array.slice();
81978221
}),
81988222
index: 0

compiled/facebook-www/ReactART-prod.classic.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ var ReactSharedInternals =
8686
enableRefAsProp = dynamicFeatureFlags.enableRefAsProp,
8787
disableDefaultPropsExceptForClasses =
8888
dynamicFeatureFlags.disableDefaultPropsExceptForClasses,
89+
enableNoCloningMemoCache = dynamicFeatureFlags.enableNoCloningMemoCache,
8990
REACT_ELEMENT_TYPE = Symbol.for("react.element"),
9091
REACT_PORTAL_TYPE = Symbol.for("react.portal"),
9192
REACT_FRAGMENT_TYPE = Symbol.for("react.fragment"),
@@ -2739,9 +2740,11 @@ function useMemoCache(size) {
27392740
((current = current.memoCache),
27402741
null != current &&
27412742
(memoCache = {
2742-
data: current.data.map(function (array) {
2743-
return array.slice();
2744-
}),
2743+
data: enableNoCloningMemoCache
2744+
? current.data
2745+
: current.data.map(function (array) {
2746+
return array.slice();
2747+
}),
27452748
index: 0
27462749
})));
27472750
}
@@ -10619,7 +10622,7 @@ var slice = Array.prototype.slice,
1061910622
return null;
1062010623
},
1062110624
bundleType: 0,
10622-
version: "19.0.0-www-classic-e5fcf28b",
10625+
version: "19.0.0-www-classic-4cca10ad",
1062310626
rendererPackageName: "react-art"
1062410627
};
1062510628
var internals$jscomp$inline_1322 = {
@@ -10650,7 +10653,7 @@ var internals$jscomp$inline_1322 = {
1065010653
scheduleRoot: null,
1065110654
setRefreshHandler: null,
1065210655
getCurrentFiber: null,
10653-
reconcilerVersion: "19.0.0-www-classic-e5fcf28b"
10656+
reconcilerVersion: "19.0.0-www-classic-4cca10ad"
1065410657
};
1065510658
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
1065610659
var hook$jscomp$inline_1323 = __REACT_DEVTOOLS_GLOBAL_HOOK__;

compiled/facebook-www/ReactART-prod.modern.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ var ReactSharedInternals =
8686
enableRefAsProp = dynamicFeatureFlags.enableRefAsProp,
8787
disableDefaultPropsExceptForClasses =
8888
dynamicFeatureFlags.disableDefaultPropsExceptForClasses,
89+
enableNoCloningMemoCache = dynamicFeatureFlags.enableNoCloningMemoCache,
8990
REACT_ELEMENT_TYPE = Symbol.for("react.element"),
9091
REACT_PORTAL_TYPE = Symbol.for("react.portal"),
9192
REACT_FRAGMENT_TYPE = Symbol.for("react.fragment"),
@@ -2537,9 +2538,11 @@ function useMemoCache(size) {
25372538
((current = current.memoCache),
25382539
null != current &&
25392540
(memoCache = {
2540-
data: current.data.map(function (array) {
2541-
return array.slice();
2542-
}),
2541+
data: enableNoCloningMemoCache
2542+
? current.data
2543+
: current.data.map(function (array) {
2544+
return array.slice();
2545+
}),
25432546
index: 0
25442547
})));
25452548
}
@@ -10098,7 +10101,7 @@ var slice = Array.prototype.slice,
1009810101
return null;
1009910102
},
1010010103
bundleType: 0,
10101-
version: "19.0.0-www-modern-20a4f736",
10104+
version: "19.0.0-www-modern-c36cdef3",
1010210105
rendererPackageName: "react-art"
1010310106
};
1010410107
var internals$jscomp$inline_1307 = {
@@ -10129,7 +10132,7 @@ var internals$jscomp$inline_1307 = {
1012910132
scheduleRoot: null,
1013010133
setRefreshHandler: null,
1013110134
getCurrentFiber: null,
10132-
reconcilerVersion: "19.0.0-www-modern-20a4f736"
10135+
reconcilerVersion: "19.0.0-www-modern-c36cdef3"
1013310136
};
1013410137
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
1013510138
var hook$jscomp$inline_1308 = __REACT_DEVTOOLS_GLOBAL_HOOK__;

compiled/facebook-www/ReactDOM-dev.classic.js

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,8 @@ var enableTrustedTypesIntegration = dynamicFeatureFlags.enableTrustedTypesIntegr
126126
enableRenderableContext = dynamicFeatureFlags.enableRenderableContext,
127127
enableRefAsProp = dynamicFeatureFlags.enableRefAsProp,
128128
favorSafetyOverHydrationPerf = dynamicFeatureFlags.favorSafetyOverHydrationPerf,
129-
disableDefaultPropsExceptForClasses = dynamicFeatureFlags.disableDefaultPropsExceptForClasses; // On WWW, false is used for a new modern build.
129+
disableDefaultPropsExceptForClasses = dynamicFeatureFlags.disableDefaultPropsExceptForClasses,
130+
enableNoCloningMemoCache = dynamicFeatureFlags.enableNoCloningMemoCache; // On WWW, false is used for a new modern build.
130131
var enableProfilerTimer = true;
131132
var enableProfilerCommitHooks = true;
132133
var enableProfilerNestedUpdatePhase = true;
@@ -12043,7 +12044,30 @@ function useMemoCache(size) {
1204312044

1204412045
if (currentMemoCache != null) {
1204512046
memoCache = {
12046-
data: currentMemoCache.data.map(function (array) {
12047+
// When enableNoCloningMemoCache is enabled, instead of treating the
12048+
// cache as copy-on-write, like we do with fibers, we share the same
12049+
// cache instance across all render attempts, even if the component
12050+
// is interrupted before it commits.
12051+
//
12052+
// If an update is interrupted, either because it suspended or
12053+
// because of another update, we can reuse the memoized computations
12054+
// from the previous attempt. We can do this because the React
12055+
// Compiler performs atomic writes to the memo cache, i.e. it will
12056+
// not record the inputs to a memoization without also recording its
12057+
// output.
12058+
//
12059+
// This gives us a form of "resuming" within components and hooks.
12060+
//
12061+
// This only works when updating a component that already mounted.
12062+
// It has no impact during initial render, because the memo cache is
12063+
// stored on the fiber, and since we have not implemented resuming
12064+
// for fibers, it's always a fresh memo cache, anyway.
12065+
//
12066+
// However, this alone is pretty useful — it happens whenever you
12067+
// update the UI with fresh data after a mutation/action, which is
12068+
// extremely common in a Suspense-driven (e.g. RSC or Relay) app.
12069+
data: enableNoCloningMemoCache ? currentMemoCache.data : // Clone the memo cache before each render (copy-on-write)
12070+
currentMemoCache.data.map(function (array) {
1204712071
return array.slice();
1204812072
}),
1204912073
index: 0
@@ -30808,7 +30832,7 @@ identifierPrefix, onUncaughtError, onCaughtError, onRecoverableError, transition
3080830832
return root;
3080930833
}
3081030834

30811-
var ReactVersion = '19.0.0-www-classic-bf2e83d8';
30835+
var ReactVersion = '19.0.0-www-classic-3bfa22a0';
3081230836

3081330837
function createPortal$1(children, containerInfo, // TODO: figure out the API for cross-renderer implementation.
3081430838
implementation) {

0 commit comments

Comments
 (0)