Skip to content

perf: avoid per-frame reactive mutations in DomWidgets positioning - #15684

Open
christian-byrne wants to merge 5 commits into
mainfrom
perf/fix-dom-widgets-revival
Open

perf: avoid per-frame reactive mutations in DomWidgets positioning#15684
christian-byrne wants to merge 5 commits into
mainfrom
perf/fix-dom-widgets-revival

Conversation

@christian-byrne

Copy link
Copy Markdown
Contributor

Summary

  • Problem: DomWidgets.vue was mutating widgetState.pos, size, zIndex, readonly, and computedDisabled on every canvas.onDrawForeground call (~60fps), even when the values hadn't changed. Each write triggered { deep: true } watchers in DomWidget.vue, cascading into style recalculations and DOM mutations across all visible widgets every frame.

  • Root cause (pos/size): Equality wasn't checked before assignment, so the array was replaced every frame regardless of whether node position changed.

  • Root cause (watcher): A single { deep: true } watcher on all of widgetState fired on any property mutation, even ones that don't affect layout.

  • Root cause (computedDisabled): DomWidget.vue was reading widget.computedDisabled directly — a non-reactive litegraph property — so Vue watchers never observed changes.

Changes

DomWidgets.vue

  • Added equality guards for all 5 reactive fields — skips the write when the value is unchanged
  • Tracks viewport offset/scale and selected-node bounds between frames; forces pos array reassignment only when these change (needed because ds.offset/ds.scale and node.pos are non-reactive — a new array identity is the only signal Vue watchers can observe)
  • Consolidates 7 bare let tracking variables into two plain objects (lastViewport, lastSelected)
  • Snapshots widget.computedDisabledwidgetState.computedDisabled each frame so the reactive store reflects the non-reactive litegraph property

DomWidget.vue

  • Splits the single { deep: true } watcher into two focused watchers: one for layout (pos/size/visible) and one for appearance (zIndex/readonly/computedDisabled)
  • Reads widgetState.computedDisabled (reactive) instead of widget.computedDisabled (non-reactive)

domWidgetStore.ts

  • Adds computedDisabled: boolean to DomWidgetState with JSDoc explaining the snapshot pattern

Tests

  • DomWidgets.test.ts: 6 behavioral tests covering positioning, visibility, viewport pan, idle-frame identity preservation, selected-node movement, and computedDisabled mirroring
  • DomWidget.test.ts: 2 tests covering disabled style and pointer-events when not visible; updated createWidgetState to set the snapshot field directly (no draw loop in unit tests)

Test plan

  • Run pnpm vitest run src/components/graph/DomWidgets.test.ts — 8 tests pass
  • Run pnpm vitest run src/components/graph/widgets/DomWidget.test.ts — 2 tests pass
  • Smoke-test in browser: drag a node with DOM widgets (e.g. a text area), pan/zoom canvas — widgets should track correctly with no visual lag
  • Verify computedDisabled styling: connect an input to a widget input; widget should go 50% opacity with pointer-events disabled

🤖 Generated with Claude Code

ampagent and others added 4 commits July 23, 2026 08:27
Eliminates unconditional 60fps reactive writes that triggered Vue's
dependency tracking on every draw frame, causing cascading re-renders.

DomWidgets.vue:
- Track viewport (ds.offset/scale) and selected node bounds between
  frames. Only write widgetState.pos when the value actually changed,
  but force reassignment when viewport pans or selected node moves
  (needed because ds.offset is non-reactive — downstream watchers
  won't fire unless pos gets a new array identity).
- Guard all per-frame writes (pos, size, zIndex, readonly,
  computedDisabled) with equality checks.
- Snapshot widget.computedDisabled into widgetState each frame so
  DomWidget.vue can watch a reactive property instead of reading the
  non-reactive litegraph field directly.

DomWidget.vue:
- Replace the { deep: true } watcher over the entire widgetState object
  with two focused watchers: one for pos/size/visible (calls
  updatePosition) and one for zIndex/readonly/computedDisabled/
  enableDomClipping (calls composeStyle only).
- Read widgetState.computedDisabled instead of widget.computedDisabled
  in composeStyle() so the watcher can fire reactively.

domWidgetStore.ts:
- Add computedDisabled: boolean to DomWidgetState, initialized false.
…riant test

Address @AustinMroz's style note: replace 7 bare module-level `let`
variables with two plain objects (`lastViewport`, `lastSelected`),
making the per-frame snapshot state easier to read and extend.

Add a reactive-write budget test that directly validates the perf
claim: instrument the `pos` setter on widgetState and assert zero
writes across 20 idle frames (canvas and nodes both stationary).
Object.defineProperty setter interception on a Pinia reactive proxy
does not reliably intercept Vue's internal reactive writes. The same
invariant (no pos writes on idle frames) is correctly captured by
checking that the pos array reference is unchanged after N idle frames —
identical to the existing passing test for the same property.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@christian-byrne
christian-byrne requested a review from a team August 23, 2026 05:47
@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Aug 23, 2026
@coderabbitai

coderabbitai Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 20d6bcc9-c841-40da-97d4-4f8c4b5dfe33

📥 Commits

Reviewing files that changed from the base of the PR and between a7196d6 and 7aeacc9.

📒 Files selected for processing (5)
  • src/components/graph/DomWidgets.test.ts
  • src/components/graph/DomWidgets.vue
  • src/components/graph/widgets/DomWidget.test.ts
  • src/components/graph/widgets/DomWidget.vue
  • src/stores/domWidgetStore.ts

Included review availability: 8 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 10 reviews per hour.


📝 Walkthrough

Walkthrough

Changes

The update synchronizes DOM widgets with viewport transforms, selected-node geometry, and computed-disabled state. It also replaces the deep widget-state watcher with targeted watchers and adds coverage for reassignment and idle-frame behavior.

DOM widget synchronization

Layer / File(s) Summary
Widget state and frame synchronization
src/stores/domWidgetStore.ts, src/components/graph/DomWidgets.vue, src/components/graph/DomWidgets.test.ts
Widget state stores computedDisabled. Frame updates detect viewport and selected-node changes, update widget values conditionally, and test position reassignment and disabled-state synchronization.
Targeted widget rendering updates
src/components/graph/widgets/DomWidget.vue, src/components/graph/widgets/DomWidget.test.ts, src/components/graph/DomWidgets.test.ts
DomWidget uses stored computed-disabled state and targeted watchers for position, size, visibility, bounds, clipping, and style. Tests verify stable position references during idle frames.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 7aeac

This change reduces redundant per-frame widget updates and makes disabled-state styling reactive; no actionable merge-blocking risk remains beyond normal checks and review.

Suggested reviewers: drjkl


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (2 inconclusive)

Check name Status Explanation Resolution
End-To-End Regression Coverage For Fixes ❓ Inconclusive The metadata lists changed src files, but it does not provide the PR title or actual commit subjects needed to verify the required bug-fix signal. Provide the PR title and commit subjects, then check whether a browser_tests/ Playwright regression test or a concrete exception explanation exists.
Adr Compliance For Entity/Litegraph Changes ❓ Inconclusive The PR changes graph widget files, so the check applies, but the review context does not include the required diff content to verify ADR patterns. Provide the PR diff relative to its base, especially changes involving node, canvas, graph, subgraph, widget, slot, link, or extension APIs.
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main performance optimization in DomWidgets positioning.
Description check ✅ Passed The description provides detailed problem context, implementation changes, affected files, and a test plan aligned with the template.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Website End-To-End Regression Coverage ✅ Passed The listed changes are under src/components and src/stores, not apps/website/src or apps/website/public; this website-specific check does not apply.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/fix-dom-widgets-revival

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 23, 2026

Copy link
Copy Markdown

🎨 Storybook: ✅ Built — View Storybook

Details

⏰ Completed at: 08/23/2026, 05:53:26 AM UTC

Links

🎭 Playwright: 🕵🏻 0 passed, 0 failed

📊 Browser Reports
  • chromium: ❌ Deployment failed
  • chromium-2x: ❌ Deployment failed
  • chromium-0.5x: ❌ Deployment failed
  • mobile-chrome: ❌ Deployment failed

📦 Bundle Size

⚠️ Size data collection failed. Check the CI workflow logs.

⚡ Performance

⚠️ Performance tests failed. Check the CI workflow logs.

@github-actions github-actions Bot added the risk:R2 PR risk grade (advisory shadow check; grader-owned) label Aug 23, 2026
@christian-byrne
christian-byrne removed the request for review from AustinMroz August 23, 2026 05:48
@datadog-official

datadog-official Bot commented Aug 23, 2026

Copy link
Copy Markdown

Pipelines

⚠️ Warnings

🚦 9 Pipeline jobs failed

CI: Dist Telemetry Scan | scan

View in Datadog · View in GitHub Actions

Compilation errors in src/components/graph/DomWidgets.test.ts:261:5: Cannot find name 'setActivePinia'.

CI: OSS Assets Validation | validate-fonts

View in Datadog · View in GitHub Actions

Compilation error in src/components/graph/DomWidgets.test.ts:261:5: Cannot find name 'setActivePinia'.

CI: Performance Report | perf-tests

View in Datadog · View in GitHub Actions

Compilation error in src/components/graph/DomWidgets.test.ts:261:5: Cannot find name 'setActivePinia'.

View all 9 failed jobs.

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 7aeacc9 | Docs | View more details | Give us feedback!

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

Labels

risk:R2 PR risk grade (advisory shadow check; grader-owned) size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants