Skip to content

Add direct-event fast path to EventTarget dispatch (rnIsDirect)#57448

Open
rubennorte wants to merge 1 commit into
react:mainfrom
rubennorte:export-D110759162
Open

Add direct-event fast path to EventTarget dispatch (rnIsDirect)#57448
rubennorte wants to merge 1 commit into
react:mainfrom
rubennorte:export-D110759162

Conversation

@rubennorte

Copy link
Copy Markdown
Member

Summary:
Direct events (those that neither bubble nor capture, such as onLayout) were still dispatched through the full W3C event-path algorithm when using the EventTarget-based dispatcher: the path was built by walking every ancestor up to the root, and the capture phase then iterated over all of them, even though a direct event only ever fires on its target. This made direct-event dispatch scale with tree depth (O(depth)).

This introduces a "direct" event mode on Event:

  • A new RN-specific rnIsDirect option on EventInit (with a matching rnIsDirect getter). A direct event has only an AT_TARGET phase and an event path containing just the target node.
  • Construction validation: an event cannot be both rnIsDirect and bubbles; the constructor throws a TypeError for that combination.
  • When rnIsDirect is set, the event path is restricted to the target, so dispatch skips the ancestor walk and capture-phase traversal (O(1)).

The renderer opts native direct events into this fast path (behind a runtime gate) by tagging them with rnIsDirect at the dispatch site. Bubbling events that merely skip bubbling (e.g. onPointerEnter) still have a capture phase and are intentionally not treated as direct.

This is gated and defaults off, so there is no behavior change by default.

Benchmark results (median dispatch latency for the new onLayout direct-event benchmarks; all use the same stable-tree setup so depth is directly comparable):

onLayout dispatch EventTarget off (baseline) EventTarget on, direct off EventTarget on, direct on
flat (1 handler) 12.7us 20.7us 18.2us
nested 10 deep 12.7us 33.4us 18.1us
nested 50 deep 12.7us 82.2us 18.2us

Without the fast path, EventTarget dispatch of a direct event scales with tree depth (20.7us -> 33.4us -> 82.2us). The fast path restores O(1): dispatch cost is flat across depth (~18us) and the nested-50-deep case drops from ~82.2us to ~18.2us (~4.5x faster), approaching the legacy baseline (~12.7us). The EventTarget-off numbers are unchanged by the direct-events flag, confirming the flag is correctly gated.

Changelog: [Internal]

Differential Revision: D110759162

Summary:
Direct events (those that neither bubble nor capture, such as `onLayout`) were still dispatched through the full W3C event-path algorithm when using the EventTarget-based dispatcher: the path was built by walking every ancestor up to the root, and the capture phase then iterated over all of them, even though a direct event only ever fires on its target. This made direct-event dispatch scale with tree depth (O(depth)).

This introduces a "direct" event mode on `Event`:

- A new RN-specific `rnIsDirect` option on `EventInit` (with a matching `rnIsDirect` getter). A direct event has only an `AT_TARGET` phase and an event path containing just the target node.
- Construction validation: an event cannot be both `rnIsDirect` and `bubbles`; the constructor throws a `TypeError` for that combination.
- When `rnIsDirect` is set, the event path is restricted to the target, so dispatch skips the ancestor walk and capture-phase traversal (O(1)).

The renderer opts native direct events into this fast path (behind a runtime gate) by tagging them with `rnIsDirect` at the dispatch site. Bubbling events that merely skip bubbling (e.g. `onPointerEnter`) still have a capture phase and are intentionally not treated as direct.

This is gated and defaults off, so there is no behavior change by default.

Benchmark results (median dispatch latency for the new `onLayout` direct-event benchmarks; all use the same stable-tree setup so depth is directly comparable):

| onLayout dispatch | EventTarget off (baseline) | EventTarget on, direct off | EventTarget on, direct on |
| --- | --- | --- | --- |
| flat (1 handler) | 12.7us | 20.7us | 18.2us |
| nested 10 deep | 12.7us | 33.4us | 18.1us |
| nested 50 deep | 12.7us | 82.2us | 18.2us |

Without the fast path, EventTarget dispatch of a direct event scales with tree depth (20.7us -> 33.4us -> 82.2us). The fast path restores O(1): dispatch cost is flat across depth (~18us) and the nested-50-deep case drops from ~82.2us to ~18.2us (~4.5x faster), approaching the legacy baseline (~12.7us). The EventTarget-off numbers are unchanged by the direct-events flag, confirming the flag is correctly gated.

Changelog: [Internal]

Differential Revision: D110759162
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jul 6, 2026
@meta-codesync

meta-codesync Bot commented Jul 6, 2026

Copy link
Copy Markdown

@rubennorte has exported this pull request. If you are a Meta employee, you can view the originating Diff in D110759162.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. meta-exported p: Facebook Partner: Facebook Partner

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant