@samitouri / QOS-React / commits / a44e750e87

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.