perf: avoid per-frame reactive mutations in DomWidgets positioning - #15684
perf: avoid per-frame reactive mutations in DomWidgets positioning#15684christian-byrne wants to merge 6 commits into
Conversation
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.
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 36 minutes Limit details: You’ve used the included review currently available. Your 92 included PR review attempts over the past 7 days set your current allowance at 1 review per hour. Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?Wait for the limit to reset, then comment An organization admin can change what happens after included review limits in Billing. How do review limits work?CodeRabbit enforces per-developer PR review limits within each organization. For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
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. 📝 WalkthroughWalkthroughChangesThe 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
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: ⚪ Minimal · up to 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: Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (2 inconclusive)
✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
🎨 Storybook: ✅ Built — View Storybook🎭 Playwright: ✅ 1981 passed, 0 failed · 2 flaky📊 Browser Reports
📦 Bundle: 9.11 MB gzip 🔴 +451 BDetailsSummary
Category Glance App Entry Points — 3.71 kB (baseline 3.71 kB) • ⚪ 0 BMain entry bundles and manifests
Status: 1 added / 1 removed Graph Workspace — 1.38 MB (baseline 1.38 MB) • 🔴 +2.34 kBGraph editor runtime, canvas, workflow orchestration
Status: 2 added / 2 removed / 1 unchanged Views & Navigation — 124 kB (baseline 124 kB) • ⚪ 0 BTop-level views, pages, and routed surfaces
Status: 13 added / 13 removed / 4 unchanged Panels & Settings — 591 kB (baseline 591 kB) • ⚪ 0 BConfiguration panels, inspectors, and settings screens
Status: 11 added / 11 removed / 16 unchanged User & Accounts — 27.5 kB (baseline 27.5 kB) • ⚪ 0 BAuthentication, profile, and account management bundles
Status: 6 added / 6 removed / 5 unchanged Editors & Dialogs — 125 kB (baseline 125 kB) • ⚪ 0 BModals, dialogs, drawers, and in-app editors
Status: 7 added / 7 removed / 1 unchanged UI Components — 67.1 kB (baseline 67.1 kB) • ⚪ 0 BReusable component library chunks
Status: 6 added / 6 removed / 8 unchanged Data & Services — 3.53 MB (baseline 3.53 MB) • 🔴 +28 BStores, services, APIs, and repositories
Status: 14 added / 14 removed / 3 unchanged Utilities & Hooks — 549 kB (baseline 549 kB) • ⚪ 0 BHelpers, composables, and utility bundles
Status: 18 added / 18 removed / 19 unchanged Vendor & Third-Party — 18.1 MB (baseline 18.1 MB) • ⚪ 0 BExternal libraries and shared vendor chunks Status: 18 unchanged Other — 14.1 MB (baseline 14.1 MB) • ⚪ 0 BBundles that do not match a named category
Status: 66 added / 66 removed / 219 unchanged ⚡ Performance Report
Show regressions
All metrics
Historical variance (last 15 runs)
Trend (last 15 commits on main)
Raw data{
"timestamp": "2026-08-23T08:54:22.609Z",
"gitSha": "9431203af729f80f89d642a4ef8e9222b4482a9a",
"branch": "perf/fix-dom-widgets-revival",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2039.9120000000153,
"styleRecalcs": 7,
"styleRecalcDurationMs": 6.891000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 532.083,
"heapDeltaBytes": -5674388,
"heapUsedBytes": 55369648,
"domNodes": -285,
"jsHeapTotalBytes": 4710400,
"scriptDurationMs": 6.456999999999999,
"eventListeners": -151,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-idle",
"durationMs": 2031.6740000000664,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.472999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 383.811,
"heapDeltaBytes": 1751336,
"heapUsedBytes": 63552264,
"domNodes": -281,
"jsHeapTotalBytes": 4448256,
"scriptDurationMs": 6.349,
"eventListeners": -181,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1753.6640000000148,
"styleRecalcs": 70,
"styleRecalcDurationMs": 31.9,
"layouts": 12,
"layoutDurationMs": 3.347,
"taskDurationMs": 798.972,
"heapDeltaBytes": 18477688,
"heapUsedBytes": 81055888,
"domNodes": -283,
"jsHeapTotalBytes": 5234688,
"scriptDurationMs": 105.30799999999999,
"eventListeners": -153,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1872.9130000000396,
"styleRecalcs": 75,
"styleRecalcDurationMs": 36.473,
"layouts": 12,
"layoutDurationMs": 3.5309999999999993,
"taskDurationMs": 808.13,
"heapDeltaBytes": 7197320,
"heapUsedBytes": 69705292,
"domNodes": -281,
"jsHeapTotalBytes": 4710400,
"scriptDurationMs": 107.704,
"eventListeners": -183,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1747.2629999999754,
"styleRecalcs": 31,
"styleRecalcDurationMs": 16.358,
"layouts": 6,
"layoutDurationMs": 0.5409999999999998,
"taskDurationMs": 351.824,
"heapDeltaBytes": 2760036,
"heapUsedBytes": 63783472,
"domNodes": 76,
"jsHeapTotalBytes": 4980736,
"scriptDurationMs": 8.875000000000002,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1666.4779999999837,
"styleRecalcs": 29,
"styleRecalcDurationMs": 14.750000000000002,
"layouts": 6,
"layoutDurationMs": 0.5139999999999999,
"taskDurationMs": 334.02,
"heapDeltaBytes": 3266896,
"heapUsedBytes": 66004408,
"domNodes": 76,
"jsHeapTotalBytes": 5242880,
"scriptDurationMs": 8.866000000000003,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "dom-widget-clipping",
"durationMs": 572.0770000000357,
"styleRecalcs": 11,
"styleRecalcDurationMs": 8.432999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 350.48900000000003,
"heapDeltaBytes": 9767156,
"heapUsedBytes": 71876444,
"domNodes": 18,
"jsHeapTotalBytes": 4980736,
"scriptDurationMs": 57.71799999999999,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "dom-widget-clipping",
"durationMs": 557.1959999999763,
"styleRecalcs": 11,
"styleRecalcDurationMs": 7.366999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 340.00399999999996,
"heapDeltaBytes": 9420068,
"heapUsedBytes": 71895188,
"domNodes": 18,
"jsHeapTotalBytes": 4980736,
"scriptDurationMs": 53.731,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-idle",
"durationMs": 2069.627999999966,
"styleRecalcs": 8,
"styleRecalcDurationMs": 8.214000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 613.3360000000001,
"heapDeltaBytes": 718828,
"heapUsedBytes": 77016152,
"domNodes": -265,
"jsHeapTotalBytes": -2101248,
"scriptDurationMs": 16.105,
"eventListeners": -147,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-idle",
"durationMs": 2039.034000000015,
"styleRecalcs": 8,
"styleRecalcDurationMs": 8.127000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 583.9609999999999,
"heapDeltaBytes": -4036392,
"heapUsedBytes": 72306976,
"domNodes": -278,
"jsHeapTotalBytes": -4096,
"scriptDurationMs": 12.941,
"eventListeners": -151,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-pan",
"durationMs": 2188.725999999974,
"styleRecalcs": 67,
"styleRecalcDurationMs": 12.983000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1177.98,
"heapDeltaBytes": -7787368,
"heapUsedBytes": 69604352,
"domNodes": -283,
"jsHeapTotalBytes": -303104,
"scriptDurationMs": 332.432,
"eventListeners": -149,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-pan",
"durationMs": 2149.2250000000013,
"styleRecalcs": 69,
"styleRecalcDurationMs": 15.107,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1162.306,
"heapDeltaBytes": -7826752,
"heapUsedBytes": 69499788,
"domNodes": -279,
"jsHeapTotalBytes": -565248,
"scriptDurationMs": 330.695,
"eventListeners": -151,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3113.8240000000224,
"styleRecalcs": 63,
"styleRecalcDurationMs": 12.34,
"layouts": 60,
"layoutDurationMs": 7.083000000000001,
"taskDurationMs": 1298.143,
"heapDeltaBytes": -8539456,
"heapUsedBytes": 69898156,
"domNodes": 8,
"jsHeapTotalBytes": 6066176,
"scriptDurationMs": 374.979,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666636,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3107.6120000000174,
"styleRecalcs": 64,
"styleRecalcDurationMs": 13.258,
"layouts": 60,
"layoutDurationMs": 6.749,
"taskDurationMs": 1350.208,
"heapDeltaBytes": -2631076,
"heapUsedBytes": 75949472,
"domNodes": -275,
"jsHeapTotalBytes": -1576960,
"scriptDurationMs": 374.191,
"eventListeners": -145,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "legacy-node-drag",
"durationMs": 2323.601999999994,
"styleRecalcs": 45,
"styleRecalcDurationMs": 9.270999999999997,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1479.011,
"heapDeltaBytes": -15206524,
"heapUsedBytes": 64757588,
"domNodes": -251,
"jsHeapTotalBytes": -4096,
"scriptDurationMs": 468.97799999999995,
"eventListeners": 27,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "legacy-node-drag",
"durationMs": 2299.705000000017,
"styleRecalcs": 47,
"styleRecalcDurationMs": 14.708000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1446.8790000000001,
"heapDeltaBytes": -20474072,
"heapUsedBytes": 64079060,
"domNodes": -245,
"jsHeapTotalBytes": 6025216,
"scriptDurationMs": 464.285,
"eventListeners": 31,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "minimap-idle",
"durationMs": 2017.601999999954,
"styleRecalcs": 9,
"styleRecalcDurationMs": 7.067,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 584.475,
"heapDeltaBytes": -9599452,
"heapUsedBytes": 68686088,
"domNodes": -284,
"jsHeapTotalBytes": -528384,
"scriptDurationMs": 14.325000000000001,
"eventListeners": -149,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "minimap-idle",
"durationMs": 2053.9150000000745,
"styleRecalcs": 6,
"styleRecalcDurationMs": 5.635000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 591.0549999999998,
"heapDeltaBytes": -8494288,
"heapUsedBytes": 74175216,
"domNodes": -280,
"jsHeapTotalBytes": 3928064,
"scriptDurationMs": 15.354999999999993,
"eventListeners": -147,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 560.8780000000024,
"styleRecalcs": 46,
"styleRecalcDurationMs": 10.188999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 374.77400000000006,
"heapDeltaBytes": 11455464,
"heapUsedBytes": 74270808,
"domNodes": 18,
"jsHeapTotalBytes": 4980736,
"scriptDurationMs": 115.43900000000001,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 618.4700000000021,
"styleRecalcs": 46,
"styleRecalcDurationMs": 9.254000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 402.15399999999994,
"heapDeltaBytes": 11952252,
"heapUsedBytes": 74423856,
"domNodes": 18,
"jsHeapTotalBytes": 4980736,
"scriptDurationMs": 132.145,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-idle",
"durationMs": 2005.8439999999678,
"styleRecalcs": 9,
"styleRecalcDurationMs": 7.802999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 463.732,
"heapDeltaBytes": 18395688,
"heapUsedBytes": 81181064,
"domNodes": -280,
"jsHeapTotalBytes": 4448256,
"scriptDurationMs": 5.771000000000002,
"eventListeners": -151,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-idle",
"durationMs": 2021.9909999999572,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.933000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 471.93100000000004,
"heapDeltaBytes": 18512516,
"heapUsedBytes": 81327312,
"domNodes": -280,
"jsHeapTotalBytes": 4710400,
"scriptDurationMs": 5.878999999999999,
"eventListeners": -151,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1691.2110000000098,
"styleRecalcs": 74,
"styleRecalcDurationMs": 33.419000000000004,
"layouts": 16,
"layoutDurationMs": 4.077000000000001,
"taskDurationMs": 753.3539999999999,
"heapDeltaBytes": 17839024,
"heapUsedBytes": 80492264,
"domNodes": -283,
"jsHeapTotalBytes": 4710400,
"scriptDurationMs": 83.30600000000001,
"eventListeners": -151,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1707.5659999999289,
"styleRecalcs": 75,
"styleRecalcDurationMs": 35.006,
"layouts": 16,
"layoutDurationMs": 4.685,
"taskDurationMs": 774.726,
"heapDeltaBytes": -6818708,
"heapUsedBytes": 55626548,
"domNodes": -282,
"jsHeapTotalBytes": 5234688,
"scriptDurationMs": 85.21,
"eventListeners": -151,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-transition-enter",
"durationMs": 1383.7109999999484,
"styleRecalcs": 18,
"styleRecalcDurationMs": 31.502000000000002,
"layouts": 13,
"layoutDurationMs": 14.727,
"taskDurationMs": 889.875,
"heapDeltaBytes": -7736860,
"heapUsedBytes": 88985716,
"domNodes": 13673,
"jsHeapTotalBytes": 13369344,
"scriptDurationMs": 15.547000000000006,
"eventListeners": 2373,
"totalBlockingTimeMs": 141,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.80000000000109
},
{
"name": "viewport-pan-sweep",
"durationMs": 8225.696000000027,
"styleRecalcs": 249,
"styleRecalcDurationMs": 35.477,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4030.3179999999993,
"heapDeltaBytes": -6724912,
"heapUsedBytes": 69918816,
"domNodes": -283,
"jsHeapTotalBytes": 4939776,
"scriptDurationMs": 1019.0360000000001,
"eventListeners": -163,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "viewport-pan-sweep",
"durationMs": 8196.198999999979,
"styleRecalcs": 249,
"styleRecalcDurationMs": 35.346,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4012.6400000000003,
"heapDeltaBytes": 7063136,
"heapUsedBytes": 83224476,
"domNodes": -239,
"jsHeapTotalBytes": -827392,
"scriptDurationMs": 1002.3729999999999,
"eventListeners": -131,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 16720.534000000043,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 16134.619,
"heapDeltaBytes": -34795064,
"heapUsedBytes": 177280188,
"domNodes": -8312,
"jsHeapTotalBytes": -15552512,
"scriptDurationMs": 111.28600000000002,
"eventListeners": -16389,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-idle",
"durationMs": 16897.94500000005,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 16235.047999999999,
"heapDeltaBytes": -35128868,
"heapUsedBytes": 177024860,
"domNodes": -8312,
"jsHeapTotalBytes": -16556032,
"scriptDurationMs": 101.929,
"eventListeners": -16361,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-pan",
"durationMs": 19861.977000000024,
"styleRecalcs": 168,
"styleRecalcDurationMs": 15.982999999999969,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 19461.367,
"heapDeltaBytes": -26677816,
"heapUsedBytes": 193772676,
"domNodes": -8312,
"jsHeapTotalBytes": -24584192,
"scriptDurationMs": 425.74,
"eventListeners": -16391,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.780000000000047,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 20525.571000000014,
"styleRecalcs": 179,
"styleRecalcDurationMs": 24.351999999999986,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 19915.955,
"heapDeltaBytes": -24503516,
"heapUsedBytes": 194073956,
"domNodes": -8312,
"jsHeapTotalBytes": -21458944,
"scriptDurationMs": 453.847,
"eventListeners": -16387,
"totalBlockingTimeMs": 137,
"frameDurationMs": 17.223333333333358,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "workflow-execution",
"durationMs": 469.32299999991756,
"styleRecalcs": 18,
"styleRecalcDurationMs": 21.198999999999998,
"layouts": 2,
"layoutDurationMs": 1.0080000000000002,
"taskDurationMs": 106.93300000000002,
"heapDeltaBytes": 4986100,
"heapUsedBytes": 67149580,
"domNodes": 150,
"jsHeapTotalBytes": 262144,
"scriptDurationMs": 6.696000000000002,
"eventListeners": 99,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "workflow-execution",
"durationMs": 480.1800000000185,
"styleRecalcs": 18,
"styleRecalcDurationMs": 20.365000000000002,
"layouts": 3,
"layoutDurationMs": 1.1540000000000001,
"taskDurationMs": 100.304,
"heapDeltaBytes": 4966952,
"heapUsedBytes": 67363268,
"domNodes": 145,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 7.012,
"eventListeners": 99,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
}
]
} |
This comment has been minimized.
This comment has been minimized.
Restore createTestingPinia/setActivePinia/beforeEach imports in DomWidgets.test.ts: main's vitest mock-cleanup (#14836) removed them from its copy, and the auto-merge combined main's import block with this branch's new perf-invariant tests that use them, breaking the CI merge-commit typecheck (TS2304).
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## main #15684 +/- ##
==========================================
+ Coverage 79.39% 81.88% +2.48%
==========================================
Files 2218 1888 -330
Lines 112288 107338 -4950
Branches 32481 31318 -1163
==========================================
- Hits 89155 87893 -1262
+ Misses 22656 19110 -3546
+ Partials 477 335 -142
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 338 files with indirect coverage changes 🚀 New features to boost your workflow:
|
ae0b310 to
e3440af
Compare
Summary
Problem:
DomWidgets.vuewas mutatingwidgetState.pos,size,zIndex,readonly, andcomputedDisabledon everycanvas.onDrawForegroundcall (~60fps), even when the values hadn't changed. Each write triggered{ deep: true }watchers inDomWidget.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 ofwidgetStatefired on any property mutation, even ones that don't affect layout.Root cause (computedDisabled):
DomWidget.vuewas readingwidget.computedDisableddirectly — a non-reactive litegraph property — so Vue watchers never observed changes.Changes
DomWidgets.vueposarray reassignment only when these change (needed becauseds.offset/ds.scaleandnode.posare non-reactive — a new array identity is the only signal Vue watchers can observe)lettracking variables into two plain objects (lastViewport,lastSelected)widget.computedDisabled→widgetState.computedDisabledeach frame so the reactive store reflects the non-reactive litegraph propertyDomWidget.vue{ deep: true }watcher into two focused watchers: one for layout (pos/size/visible) and one for appearance (zIndex/readonly/computedDisabled)widgetState.computedDisabled(reactive) instead ofwidget.computedDisabled(non-reactive)domWidgetStore.tscomputedDisabled: booleantoDomWidgetStatewith JSDoc explaining the snapshot patternTests
DomWidgets.test.ts: 6 behavioral tests covering positioning, visibility, viewport pan, idle-frame identity preservation, selected-node movement, and computedDisabled mirroringDomWidget.test.ts: 2 tests covering disabled style and pointer-events when not visible; updatedcreateWidgetStateto set the snapshot field directly (no draw loop in unit tests)Test plan
pnpm vitest run src/components/graph/DomWidgets.test.ts— 8 tests passpnpm vitest run src/components/graph/widgets/DomWidget.test.ts— 2 tests passcomputedDisabledstyling: connect an input to a widget input; widget should go 50% opacity with pointer-events disabled🤖 Generated with Claude Code