A framework for building native applications using React
Cache parent lookups during event dispatch (#56853)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/56853 The W3C event-dispatch pipeline (gated on `enableNativeEventTargetEventDispatching`) walks the parent chain of the dispatched target on every event: once in `EventTarget.getEventPath` for the capture/bubble path, plus up to two additional walks in the responder system for touch/scroll/selection events. Each `parentNode` read is a JSI hop into C++ that re-walks the family chain in the current shadow-tree revision, so on event-heavy screens (e.g., a list firing many `onLayout` events during mount) the per-event walk cost adds up. This change adds a per-instance parent cache that all event-dispatch consumers share. RN host trees are append-only and the shadow tree is stable during dispatch, so once a node is reachable from the dispatch path its parent is permanently stable from that pipeline's point of view. The cache stores the resolved parent in a symbol-keyed slot on the first lookup; subsequent lookups (within the same dispatch and across future dispatches on the same tree) collapse to a property load. - Add `getEventTargetParent(target)` in `EventTargetInternals` (plus the cache slot and a sentinel for cached nulls). - Route `EventTarget.getEventPath` through the new utility. - Route the `parentElement` walks in `ReactNativeResponder` (`getLowestCommonAncestor`, `negotiateResponder` path build, and the `skipSelf` step) through the same utility, with an `instanceof ReadOnlyElement` filter so the responder's element-only invariant is preserved. - Leave the `parentNode` getter on `ReadOnlyNode` untouched — user-visible reads still take the canonical JSI path and detached-node reads continue to return `null`. - Add two stable-tree scenarios to `EventDispatching-benchmark-itest.js` (`beforeAll` mounts once, the benchmarked function dispatches per iteration) so the cache win is measurable and any regression in the rebuild-per-iter scenarios is also visible. ## Benchmark results Ran `yarn fantom EventDispatching-benchmark --benchmarks` from `xplat/js/react-native-github/`, comparing the cache disabled (utility short-circuited to the canonical getter) against the cache enabled. Numbers below are p50 latency; both runs use Hermes-bytecode optimized mode (the default for `*-benchmark-itest.js`). New stable-tree scenarios with the new pipeline (`enableNativeEventTargetEventDispatching` ON): | Scenario (depth 50, stable tree) | Cache OFF | Cache ON | Improvement | | ------------------------------------------------ | --------- | -------- | ------------------- | | dispatch event, bubbling, handlers on ancestors | 0.271 ms | 0.165 ms | 39% faster (1.64×) | | dispatch event, no handlers on ancestors | 0.265 ms | 0.161 ms | 39% faster (1.65×) | Pre-existing rebuild-per-iter scenarios with the new pipeline (no measurable change — cache is empty on each iteration): | Scenario (flag ON) | Cache OFF | Cache ON | Δ | | ------------------------------------- | --------- | -------- | - | | flat (1 handler) | 0.042 ms | 0.042 ms | — | | nested 10 deep (bubbling) | 0.105 ms | 0.106 ms | — | | nested 50 deep (bubbling) | 0.378 ms | 0.381 ms | — | | nested 10 deep (no handlers) | 0.103 ms | 0.104 ms | — | | stopPropagation, nested 10 deep | 0.089 ms | 0.091 ms | — | | render + dispatch, flat | 0.082 ms | 0.083 ms | — | Legacy pipeline (`enableNativeEventTargetEventDispatching` OFF) was unchanged across both runs, confirming the cache change does not leak outside the new pipeline. Changelog: [Internal] Reviewed By: andrewdacenko Differential Revision: D105337953 fbshipit-source-id: cd157aaa34049906de00566a901a87f0e447b644
R
Rubén Norte committed
e705e8d971ff30f2e2c91f5d672dc18dd062be74
Parent: 3b9c581
Committed by meta-codesync[bot] <215208954+meta-codesync[bot]@users.noreply.github.com>
on 5/15/2026, 7:23:02 PM