Store instance handles in an internal map behind flag (#35053)
We already append `randomKey` to each handle name to prevent external libraries from accessing and relying on these internals. But more libraries recently have been getting around this by simply iterating over the element properties and using a `startsWith` check. This flag allows us to experiment with moving these handles to an internal map. This PR starts with the two most common internals, the props object and the fiber. We can consider moving additional properties such as the container root and others depending on perf results.
Jack Pope committed
Nov 6, 2025 at 18:17 UTC
a44e750e87fd0869cdeda0418e279e19c1ee07dd
8 files changed
+91
-11
packages/react-dom-bindings/src/client/ReactDOMComponentTree.js
+79
-11
@@ -38,6 +38,8 @@ import {getParentHydrationBoundary} from './ReactFiberConfigDOM';
38
39
import {enableScopeAPI} from 'shared/ReactFeatureFlags';
40
41
+import {enableInternalInstanceMap} from 'shared/ReactFeatureFlags';
42
+
43
const randomKey = Math.random().toString(36).slice(2);
44
const internalInstanceKey = '__reactFiber$' + randomKey;
45
const internalPropsKey = '__reactProps$' + randomKey;
@@ -49,7 +51,32 @@ const internalRootNodeResourcesKey = '__reactResources$' + randomKey;
51
const internalHoistableMarker = '__reactMarker$' + randomKey;
52
const internalScrollTimer = '__reactScroll$' + randomKey;
53
54
+type InstanceUnion =
55
+ | Instance
56
+ | TextInstance
57
+ | SuspenseInstance
58
+ | ActivityInstance
59
+ | ReactScopeInstance
60
+ | Container;
61
+
62
+const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
63
+const internalInstanceMap:
64
+ | WeakMap<InstanceUnion, Fiber>
65
+ | Map<InstanceUnion, Fiber> = new PossiblyWeakMap();
66
+const internalPropsMap:
67
+ | WeakMap<InstanceUnion, Props>
68
+ | Map<InstanceUnion, Props> = new PossiblyWeakMap();
69
+
70
export function detachDeletedInstance(node: Instance): void {
71
+ if (enableInternalInstanceMap) {
72
+ internalInstanceMap.delete(node);
73
+ internalPropsMap.delete(node);
74
+ delete (node: any)[internalEventHandlersKey];
75
+ delete (node: any)[internalEventHandlerListenersKey];
76
+ delete (node: any)[internalEventHandlesSetKey];
77
+ delete (node: any)[internalRootNodeResourcesKey];
78
+ return;
79
+ }
80
// TODO: This function is only called on host components. I don't think all of
81
// these fields are relevant.
82
delete (node: any)[internalInstanceKey];
@@ -68,6 +95,10 @@ export function precacheFiberNode(
95
| ActivityInstance
96
| ReactScopeInstance,
97
): void {
98
+ if (enableInternalInstanceMap) {
99
+ internalInstanceMap.set(node, hostInst);
100
+ return;
101
+ }
102
(node: any)[internalInstanceKey] = hostInst;
103
}
104
@@ -95,7 +126,12 @@ export function isContainerMarkedAsRoot(node: Container): boolean {
126
// HostRoot back. To get to the HostRoot, you need to pass a child of it.
127
// The same thing applies to Suspense and Activity boundaries.
128
export function getClosestInstanceFromNode(targetNode: Node): null | Fiber {
98
- let targetInst = (targetNode: any)[internalInstanceKey];
129
+ let targetInst: void | Fiber;
130
+ if (enableInternalInstanceMap) {
131
+ targetInst = internalInstanceMap.get(((targetNode: any): InstanceUnion));
132
+ } else {
133
+ targetInst = (targetNode: any)[internalInstanceKey];
134
+ }
135
if (targetInst) {
136
// Don't return HostRoot, SuspenseComponent or ActivityComponent here.
137
return targetInst;
@@ -112,9 +148,15 @@ export function getClosestInstanceFromNode(targetNode: Node): null | Fiber {
148
// itself because the fibers are conceptually between the container
149
// node and the first child. It isn't surrounding the container node.
150
// If it's not a container, we check if it's an instance.
115
- targetInst =
116
- (parentNode: any)[internalContainerInstanceKey] ||
117
- (parentNode: any)[internalInstanceKey];
151
+ if (enableInternalInstanceMap) {
152
+ targetInst =
153
+ (parentNode: any)[internalContainerInstanceKey] ||
154
+ internalInstanceMap.get(((parentNode: any): InstanceUnion));
155
+ } else {
156
+ targetInst =
157
+ (parentNode: any)[internalContainerInstanceKey] ||
158
+ (parentNode: any)[internalInstanceKey];
159
+ }
160
if (targetInst) {
161
// Since this wasn't the direct target of the event, we might have
162
// stepped past dehydrated DOM nodes to get here. However they could
@@ -147,8 +189,10 @@ export function getClosestInstanceFromNode(targetNode: Node): null | Fiber {
189
// have had an internalInstanceKey on it.
190
// Let's get the fiber associated with the SuspenseComponent
191
// as the deepest instance.
150
- // $FlowFixMe[prop-missing]
151
- const targetFiber = hydrationInstance[internalInstanceKey];
192
+ const targetFiber = enableInternalInstanceMap
193
+ ? internalInstanceMap.get(hydrationInstance)
194
+ : // $FlowFixMe[prop-missing]
195
+ hydrationInstance[internalInstanceKey];
196
if (targetFiber) {
197
return targetFiber;
198
}
@@ -175,9 +219,16 @@ export function getClosestInstanceFromNode(targetNode: Node): null | Fiber {
219
* instance, or null if the node was not rendered by this React.
220
*/
221
export function getInstanceFromNode(node: Node): Fiber | null {
178
- const inst =
179
- (node: any)[internalInstanceKey] ||
180
- (node: any)[internalContainerInstanceKey];
222
+ let inst: void | null | Fiber;
223
+ if (enableInternalInstanceMap) {
224
+ inst =
225
+ internalInstanceMap.get(((node: any): InstanceUnion)) ||
226
+ (node: any)[internalContainerInstanceKey];
227
+ } else {
228
+ inst =
229
+ (node: any)[internalInstanceKey] ||
230
+ (node: any)[internalContainerInstanceKey];
231
+ }
232
if (inst) {
233
const tag = inst.tag;
234
if (
@@ -226,16 +277,24 @@ export function getFiberCurrentPropsFromNode(
277
| TextInstance
278
| SuspenseInstance
279
| ActivityInstance,
229
-): Props {
280
+): Props | null {
281
+ if (enableInternalInstanceMap) {
282
+ return internalPropsMap.get(node) || null;
283
+ }
284
return (node: any)[internalPropsKey] || null;
285
}
286
287
export function updateFiberProps(node: Instance, props: Props): void {
288
+ if (enableInternalInstanceMap) {
289
+ internalPropsMap.set(node, props);
290
+ return;
291
+ }
292
(node: any)[internalPropsKey] = props;
293
}
294
295
export function getEventListenerSet(node: EventTarget): Set<string> {
238
- let elementListenerSet = (node: any)[internalEventHandlersKey];
296
+ let elementListenerSet: Set<string> | void;
297
+ elementListenerSet = (node: any)[internalEventHandlersKey];
298
if (elementListenerSet === undefined) {
299
elementListenerSet = (node: any)[internalEventHandlersKey] = new Set();
300
}
@@ -246,6 +305,9 @@ export function getFiberFromScopeInstance(
305
scope: ReactScopeInstance,
306
): null | Fiber {
307
if (enableScopeAPI) {
308
+ if (enableInternalInstanceMap) {
309
+ return internalInstanceMap.get(((scope: any): InstanceUnion)) || null;
310
+ }
311
return (scope: any)[internalInstanceKey] || null;
312
}
313
return null;
@@ -318,6 +380,12 @@ export function clearScrollEndTimer(node: EventTarget): void {
380
}
381
382
export function isOwnedInstance(node: Node): boolean {
383
+ if (enableInternalInstanceMap) {
384
+ return !!(
385
+ (node: any)[internalHoistableMarker] ||
386
+ internalInstanceMap.has((node: any))
387
+ );
388
+ }
389
return !!(
390
(node: any)[internalHoistableMarker] || (node: any)[internalInstanceKey]
391
);
packages/shared/ReactFeatureFlags.js
+2
@@ -147,6 +147,8 @@ export const enableFragmentRefs: boolean = true;
147
export const enableFragmentRefsScrollIntoView: boolean = true;
148
export const enableFragmentRefsInstanceHandles: boolean = false;
149
150
+export const enableInternalInstanceMap: boolean = false;
151
+
152
// -----------------------------------------------------------------------------
153
// Ready for next major.
154
//
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -84,6 +84,7 @@ export const enableComponentPerformanceTrack: boolean =
84
__PROFILE__ && dynamicFlags.enableComponentPerformanceTrack;
85
export const enablePerformanceIssueReporting: boolean =
86
enableComponentPerformanceTrack;
87
+export const enableInternalInstanceMap: boolean = false;
88
89
// Flow magic to verify the exports of this file match the original version.
90
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.native-oss.js
+2
@@ -76,6 +76,8 @@ export const enableFragmentRefs: boolean = true;
76
export const enableFragmentRefsScrollIntoView: boolean = false;
77
export const enableFragmentRefsInstanceHandles: boolean = false;
78
79
+export const enableInternalInstanceMap: boolean = false;
80
+
81
// Profiling Only
82
export const enableProfilerTimer: boolean = __PROFILE__;
83
export const enableProfilerCommitHooks: boolean = __PROFILE__;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+2
@@ -77,6 +77,8 @@ export const enableFragmentRefs: boolean = true;
77
export const enableFragmentRefsScrollIntoView: boolean = true;
78
export const enableFragmentRefsInstanceHandles: boolean = false;
79
80
+export const enableInternalInstanceMap: boolean = false;
81
+
82
// TODO: This must be in sync with the main ReactFeatureFlags file because
83
// the Test Renderer's value must be the same as the one used by the
84
// react package.
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+2
@@ -85,5 +85,7 @@ export const enableFragmentRefsScrollIntoView: boolean = false;
85
export const enableFragmentRefsInstanceHandles: boolean = false;
86
export const ownerStackLimit = 1e4;
87
88
+export const enableInternalInstanceMap: boolean = false;
89
+
90
// Flow magic to verify the exports of this file match the original version.
91
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+2
@@ -38,6 +38,8 @@ export const enableFragmentRefs: boolean = __VARIANT__;
38
export const enableFragmentRefsScrollIntoView: boolean = __VARIANT__;
39
export const enableAsyncDebugInfo: boolean = __VARIANT__;
40
41
+export const enableInternalInstanceMap: boolean = __VARIANT__;
42
+
43
// TODO: These flags are hard-coded to the default values used in open source.
44
// Update the tests so that they pass in either mode, then set these
45
// to __VARIANT__.
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -35,6 +35,7 @@ export const {
35
enableFragmentRefs,
36
enableFragmentRefsScrollIntoView,
37
enableAsyncDebugInfo,
38
+ enableInternalInstanceMap,
39
} = dynamicFeatureFlags;
40
41
// On WWW, __EXPERIMENTAL__ is used for a new modern build.