perf(runtime-dom): avoid event handler array allocations#14828
Conversation
📝 WalkthroughWalkthroughThe PR refactors event dispatch in ChangesEvent Handler Array Processing
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
@vue/compiler-core
@vue/compiler-dom
@vue/compiler-sfc
@vue/compiler-ssr
@vue/reactivity
@vue/runtime-core
@vue/runtime-dom
@vue/server-renderer
@vue/shared
vue
@vue/compat
commit: |
Size ReportBundles
Usages
|
bf46e08 to
cc5d253
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cc5d253fa5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| originalStop.call(e) | ||
| ;(e as any)._stopped = true | ||
| } | ||
| for (let i = 0; i < value.length; i++) { |
There was a problem hiding this comment.
Snapshot handler array length before iterating
Iterating with i < value.length makes dispatch observe in-loop mutations to the same handler array, which changes previous behavior and can hang the event loop. If a handler appends to value (e.g. pushes itself or another callback), the loop will keep extending and newly added handlers run in the same dispatch (or never terminate), whereas the prior map path implicitly iterated a fixed snapshot. This is user-reachable whenever the listener array is reused/mutated from inside a handler.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
🧹 Nitpick comments (2)
packages/runtime-dom/src/modules/events.ts (1)
122-133: ⚡ Quick winAvoid per-handler args-array allocations in the hot loop.
This path still allocates
[e]for every handler invocation. Hoist it once per dispatch to keep the allocation reduction goal intact.♻️ Proposed fix
if (isArray(value)) { const originalStop = e.stopImmediatePropagation e.stopImmediatePropagation = () => { originalStop.call(e) ;(e as any)._stopped = true } + const args = [e] for (let i = 0; i < value.length; i++) { if ((e as any)._stopped) { break } const handler = value[i] if (handler) { callWithAsyncErrorHandling( handler, instance, ErrorCodes.NATIVE_EVENT_HANDLER, - [e], + args, ) } } } else {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/runtime-dom/src/modules/events.ts` around lines 122 - 133, The hot-loop currently allocates a new args array for every handler call by passing [e] inline; hoist that allocation out of the loop by creating a single const args = [e] immediately before the for loop and pass args into callWithAsyncErrorHandling instead. Update the loop in events.ts where value (the handlers array) is iterated and ensure callWithAsyncErrorHandling(handler, instance, ErrorCodes.NATIVE_EVENT_HANDLER, args) is used so no per-handler array allocations occur.packages/runtime-dom/__benchmarks__/events.bench.ts (1)
5-22: ⚡ Quick winBenchmark body currently mixes setup cost with dispatch cost.
Given the bench names, consider moving
createElement+patchPropsetup outside the timed callback so the metric isolates dispatch performance.📊 Proposed refactor
describe('runtime-dom events', () => { + const singleEl = document.createElement('button') + let singleCount = 0 + patchProp(singleEl, 'onClick', null, () => singleCount++) + + const multiEl = document.createElement('button') + let multiCount = 0 + patchProp(multiEl, 'onClick', null, [ + () => multiCount++, + () => multiCount++, + () => multiCount++, + () => multiCount++, + ]) + bench('dispatch click with single handler', () => { - const el = document.createElement('button') - let count = 0 - patchProp(el, 'onClick', null, () => count++) - el.dispatchEvent(new Event('click')) + singleEl.dispatchEvent(new Event('click')) }) bench('dispatch click with multiple handlers', () => { - const el = document.createElement('button') - let count = 0 - patchProp(el, 'onClick', null, [ - () => count++, - () => count++, - () => count++, - () => count++, - ]) - el.dispatchEvent(new Event('click')) + multiEl.dispatchEvent(new Event('click')) }) })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/runtime-dom/__benchmarks__/events.bench.ts` around lines 5 - 22, The benchmark callbacks currently include setup (document.createElement + patchProp) which contaminates dispatch timing; extract the setup for both benches so only el.dispatchEvent(new Event('click')) runs inside each bench callback: create the button element once per bench, call patchProp(el, 'onClick', null, ...) beforehand (for the single-handler and the array-of-handlers cases), then have the bench callback simply call el.dispatchEvent(...). Reference the existing bench(...) invocations, document.createElement, patchProp, and el.dispatchEvent when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/runtime-dom/__benchmarks__/events.bench.ts`:
- Around line 5-22: The benchmark callbacks currently include setup
(document.createElement + patchProp) which contaminates dispatch timing; extract
the setup for both benches so only el.dispatchEvent(new Event('click')) runs
inside each bench callback: create the button element once per bench, call
patchProp(el, 'onClick', null, ...) beforehand (for the single-handler and the
array-of-handlers cases), then have the bench callback simply call
el.dispatchEvent(...). Reference the existing bench(...) invocations,
document.createElement, patchProp, and el.dispatchEvent when making the change.
In `@packages/runtime-dom/src/modules/events.ts`:
- Around line 122-133: The hot-loop currently allocates a new args array for
every handler call by passing [e] inline; hoist that allocation out of the loop
by creating a single const args = [e] immediately before the for loop and pass
args into callWithAsyncErrorHandling instead. Update the loop in events.ts where
value (the handlers array) is iterated and ensure
callWithAsyncErrorHandling(handler, instance, ErrorCodes.NATIVE_EVENT_HANDLER,
args) is used so no per-handler array allocations occur.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d77a28ed-4eb2-4916-88f5-8fc6d8e8aeca
📒 Files selected for processing (3)
packages/runtime-dom/__benchmarks__/events.bench.tspackages/runtime-dom/__tests__/patchProps.spec.tspackages/runtime-dom/src/modules/events.ts
Summary
Array.mapand wrapper closure allocations for runtime-dom event handler arrays.stopImmediatePropagation()behavior by stopping later handlers in the same array.Test Plan
test / unit-testtest / unit-test-windowstest / lint-and-test-dtstest / e2e-testgit apply --checkwas run against the fetched upstream target files before creating this branch.Suggested benchmark command:
Summary by CodeRabbit
Tests
Chores