fix: RAF-batch Vue node ResizeObserver writes to layoutStore#12299
fix: RAF-batch Vue node ResizeObserver writes to layoutStore#12299christian-byrne wants to merge 3 commits into
Conversation
Coalesces multiple ResizeObserver callbacks fired during the same frame into a single layoutStore write, mirroring the pattern already used by useSlotElementTracking. Defers measurement/write until next RAF so the canvas ResizeObserver has had a chance to update lgCanvas.ds before any DOM->canvas conversion fallback runs. Fixes the case where opening the bottom panel during an animated splitter resize causes Vue workflow nodes to flicker / appear displaced for ~1-2s before the view settles, because the splitter's per-frame size changes each triggered a synchronous layoutStore write with stale viewport state.
… hop Address review: widgets-grid ResizeObserver signals were being staged in this file's RAF batch and then forwarded to scheduleSlotLayoutSync(), which RAF-batches a second time. Route them directly inside the RO callback, restoring single-RAF latency for slot drift correction and preserving the prior linearMode/hidden-tab gating behavior that the slot tracker already handles.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughResizeObserver measurements are queued into pendingMeasurements and flushed on the next animation frame via createRafBatch; the observer callback now enqueues measurements (with a widgets-grid signal path) and tests mock a controllable RAF batch and document visibility to validate deferred writes, coalescing, and hidden-tab behavior. ChangesResizeObserver RAF batching
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Caution Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional.
❌ Failed checks (1 error, 1 warning)
✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
🎨 Storybook: ✅ Built — View Storybook |
🎭 Playwright: ✅ 1604 passed, 0 failed · 2 flaky📊 Browser Reports
|
📦 Bundle: 5.36 MB gzip 🔴 +243 BDetailsSummary
Category Glance App Entry Points — 26.1 kB (baseline 26.1 kB) • ⚪ 0 BMain entry bundles and manifests
Status: 1 added / 1 removed Graph Workspace — 1.24 MB (baseline 1.24 MB) • ⚪ 0 BGraph editor runtime, canvas, workflow orchestration
Status: 1 added / 1 removed Views & Navigation — 82.9 kB (baseline 82.9 kB) • ⚪ 0 BTop-level views, pages, and routed surfaces
Status: 9 added / 9 removed / 2 unchanged Panels & Settings — 527 kB (baseline 527 kB) • ⚪ 0 BConfiguration panels, inspectors, and settings screens
Status: 10 added / 10 removed / 14 unchanged User & Accounts — 17.8 kB (baseline 17.8 kB) • ⚪ 0 BAuthentication, profile, and account management bundles
Status: 5 added / 5 removed / 2 unchanged Editors & Dialogs — 112 kB (baseline 112 kB) • ⚪ 0 BModals, dialogs, drawers, and in-app editors
Status: 4 added / 4 removed UI Components — 58 kB (baseline 58 kB) • ⚪ 0 BReusable component library chunks
Status: 5 added / 5 removed / 8 unchanged Data & Services — 3.16 MB (baseline 3.16 MB) • 🔴 +799 BStores, services, APIs, and repositories
Status: 13 added / 13 removed / 4 unchanged Utilities & Hooks — 366 kB (baseline 366 kB) • ⚪ 0 BHelpers, composables, and utility bundles
Status: 13 added / 13 removed / 18 unchanged Vendor & Third-Party — 9.94 MB (baseline 9.94 MB) • ⚪ 0 BExternal libraries and shared vendor chunks Status: 16 unchanged Other — 9.16 MB (baseline 9.16 MB) • ⚪ 0 BBundles that do not match a named category
Status: 57 added / 57 removed / 86 unchanged ⚡ Performance Report
All metrics
Historical variance (last 15 runs)
Trend (last 15 commits on main)
Raw data{
"timestamp": "2026-05-15T21:31:32.722Z",
"gitSha": "4cddcb316a47cf12d3355b0a019b784e43c15acd",
"branch": "glary/raf-batch-vue-node-resize-tracking",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2028.1359999999893,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.442000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 415.91999999999996,
"heapDeltaBytes": 23257564,
"heapUsedBytes": 71374336,
"domNodes": 20,
"jsHeapTotalBytes": 14417920,
"scriptDurationMs": 28.026,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-idle",
"durationMs": 2044.685999999956,
"styleRecalcs": 8,
"styleRecalcDurationMs": 8.122,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 400.822,
"heapDeltaBytes": 22901216,
"heapUsedBytes": 71430300,
"domNodes": 16,
"jsHeapTotalBytes": 15204352,
"scriptDurationMs": 21.47600000000001,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1970.8540000000028,
"styleRecalcs": 76,
"styleRecalcDurationMs": 45.617000000000004,
"layouts": 12,
"layoutDurationMs": 4.347,
"taskDurationMs": 944.768,
"heapDeltaBytes": 4265476,
"heapUsedBytes": 52915196,
"domNodes": -262,
"jsHeapTotalBytes": 15593472,
"scriptDurationMs": 137.03600000000003,
"eventListeners": -133,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1852.0379999999932,
"styleRecalcs": 73,
"styleRecalcDurationMs": 42.278999999999996,
"layouts": 12,
"layoutDurationMs": 4.272,
"taskDurationMs": 830.926,
"heapDeltaBytes": 1303912,
"heapUsedBytes": 49958004,
"domNodes": -264,
"jsHeapTotalBytes": 15855616,
"scriptDurationMs": 124.20799999999998,
"eventListeners": -133,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1733.9910000000032,
"styleRecalcs": 30,
"styleRecalcDurationMs": 18.819999999999997,
"layouts": 6,
"layoutDurationMs": 0.77,
"taskDurationMs": 348.18000000000006,
"heapDeltaBytes": 511628,
"heapUsedBytes": 49268004,
"domNodes": 75,
"jsHeapTotalBytes": 15466496,
"scriptDurationMs": 25.805000000000003,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1762.1919999999704,
"styleRecalcs": 32,
"styleRecalcDurationMs": 21.775000000000002,
"layouts": 6,
"layoutDurationMs": 0.915,
"taskDurationMs": 396.09700000000004,
"heapDeltaBytes": 23339200,
"heapUsedBytes": 72975508,
"domNodes": 79,
"jsHeapTotalBytes": 24641536,
"scriptDurationMs": 33.84000000000001,
"eventListeners": 21,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "dom-widget-clipping",
"durationMs": 594.1919999999925,
"styleRecalcs": 11,
"styleRecalcDurationMs": 8.459000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 388.82699999999994,
"heapDeltaBytes": 9569384,
"heapUsedBytes": 58654520,
"domNodes": 18,
"jsHeapTotalBytes": 14417920,
"scriptDurationMs": 65.845,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "dom-widget-clipping",
"durationMs": 551.1419999999703,
"styleRecalcs": 10,
"styleRecalcDurationMs": 6.898999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 342.057,
"heapDeltaBytes": 8862468,
"heapUsedBytes": 57465820,
"domNodes": 16,
"jsHeapTotalBytes": 15204352,
"scriptDurationMs": 58.03000000000001,
"eventListeners": 0,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999727
},
{
"name": "large-graph-idle",
"durationMs": 2040.2270000000158,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.989999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 599.89,
"heapDeltaBytes": 7971092,
"heapUsedBytes": 66553316,
"domNodes": -263,
"jsHeapTotalBytes": 290816,
"scriptDurationMs": 99.06400000000001,
"eventListeners": -129,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-idle",
"durationMs": 2023.2789999999454,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.139999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 598.843,
"heapDeltaBytes": 14353128,
"heapUsedBytes": 79472232,
"domNodes": 20,
"jsHeapTotalBytes": 14036992,
"scriptDurationMs": 105.47600000000001,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2169.4840000000113,
"styleRecalcs": 69,
"styleRecalcDurationMs": 21.344999999999995,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1162.4279999999999,
"heapDeltaBytes": 11220904,
"heapUsedBytes": 70878632,
"domNodes": -263,
"jsHeapTotalBytes": 5476352,
"scriptDurationMs": 399.419,
"eventListeners": -157,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-pan",
"durationMs": 2250.2420000000143,
"styleRecalcs": 69,
"styleRecalcDurationMs": 21.272999999999996,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1253.8819999999998,
"heapDeltaBytes": 50765568,
"heapUsedBytes": 110804348,
"domNodes": -259,
"jsHeapTotalBytes": 41127936,
"scriptDurationMs": 399.545,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-zoom",
"durationMs": 3232.3560000000384,
"styleRecalcs": 65,
"styleRecalcDurationMs": 21.543,
"layouts": 60,
"layoutDurationMs": 8.864,
"taskDurationMs": 1448.7259999999999,
"heapDeltaBytes": 8191336,
"heapUsedBytes": 69673716,
"domNodes": -265,
"jsHeapTotalBytes": 3756032,
"scriptDurationMs": 517.3530000000001,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-zoom",
"durationMs": 3196.388000000013,
"styleRecalcs": 65,
"styleRecalcDurationMs": 21.735,
"layouts": 60,
"layoutDurationMs": 8.940999999999999,
"taskDurationMs": 1438.002,
"heapDeltaBytes": 14058312,
"heapUsedBytes": 75767824,
"domNodes": -267,
"jsHeapTotalBytes": 3493888,
"scriptDurationMs": 519.574,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "minimap-idle",
"durationMs": 2103.6040000000185,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.715000000000003,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 695.451,
"heapDeltaBytes": 44657252,
"heapUsedBytes": 106069924,
"domNodes": -265,
"jsHeapTotalBytes": 32796672,
"scriptDurationMs": 108.561,
"eventListeners": -129,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "minimap-idle",
"durationMs": 2037.3710000000074,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.491999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 700.6680000000001,
"heapDeltaBytes": 43379456,
"heapUsedBytes": 103190240,
"domNodes": -262,
"jsHeapTotalBytes": 32796672,
"scriptDurationMs": 114.182,
"eventListeners": -129,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 588.4759999999574,
"styleRecalcs": 47,
"styleRecalcDurationMs": 11.745999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 386.31499999999994,
"heapDeltaBytes": 9146564,
"heapUsedBytes": 58097076,
"domNodes": 19,
"jsHeapTotalBytes": 15466496,
"scriptDurationMs": 136.383,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 850.166999999999,
"styleRecalcs": 48,
"styleRecalcDurationMs": 16.136,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 559.7959999999999,
"heapDeltaBytes": -4403208,
"heapUsedBytes": 62191824,
"domNodes": 21,
"jsHeapTotalBytes": 23425024,
"scriptDurationMs": 162.71999999999997,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666682,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-idle",
"durationMs": 2020.8519999999908,
"styleRecalcs": 11,
"styleRecalcDurationMs": 11.449999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 432.84399999999994,
"heapDeltaBytes": 1551688,
"heapUsedBytes": 68223900,
"domNodes": 22,
"jsHeapTotalBytes": 18706432,
"scriptDurationMs": 27.166000000000004,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-idle",
"durationMs": 2006.621999999993,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.873,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 407.903,
"heapDeltaBytes": 23116272,
"heapUsedBytes": 71807400,
"domNodes": 17,
"jsHeapTotalBytes": 14942208,
"scriptDurationMs": 19.813,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1740.554000000003,
"styleRecalcs": 76,
"styleRecalcDurationMs": 41.909,
"layouts": 16,
"layoutDurationMs": 5.084,
"taskDurationMs": 809.452,
"heapDeltaBytes": -21303100,
"heapUsedBytes": 47235776,
"domNodes": -258,
"jsHeapTotalBytes": 26173440,
"scriptDurationMs": 104.70700000000001,
"eventListeners": -133,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 2022.8270000000066,
"styleRecalcs": 85,
"styleRecalcDurationMs": 49.272999999999996,
"layouts": 16,
"layoutDurationMs": 7.175,
"taskDurationMs": 974.713,
"heapDeltaBytes": -360912,
"heapUsedBytes": 48499844,
"domNodes": -263,
"jsHeapTotalBytes": 15593472,
"scriptDurationMs": 99.74699999999999,
"eventListeners": -133,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-transition-enter",
"durationMs": 1150.811000000033,
"styleRecalcs": 15,
"styleRecalcDurationMs": 28.343999999999994,
"layouts": 4,
"layoutDurationMs": 11.741999999999999,
"taskDurationMs": 899.9710000000001,
"heapDeltaBytes": 81823660,
"heapUsedBytes": 224268912,
"domNodes": 13513,
"jsHeapTotalBytes": 80478208,
"scriptDurationMs": 34.063999999999986,
"eventListeners": 2527,
"totalBlockingTimeMs": 156,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "viewport-pan-sweep",
"durationMs": 8192.68999999997,
"styleRecalcs": 250,
"styleRecalcDurationMs": 61.098000000000006,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3949.1100000000006,
"heapDeltaBytes": 15958232,
"heapUsedBytes": 75757448,
"domNodes": -260,
"jsHeapTotalBytes": 4718592,
"scriptDurationMs": 1258.775,
"eventListeners": -113,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "viewport-pan-sweep",
"durationMs": 8206.04000000003,
"styleRecalcs": 249,
"styleRecalcDurationMs": 62.324000000000005,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4029.863,
"heapDeltaBytes": 13183852,
"heapUsedBytes": 71962224,
"domNodes": -262,
"jsHeapTotalBytes": 7573504,
"scriptDurationMs": 1282.2469999999998,
"eventListeners": -113,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.80000000000109
},
{
"name": "vue-large-graph-idle",
"durationMs": 13731.302999999969,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 13719.362,
"heapDeltaBytes": -39939984,
"heapUsedBytes": 169736876,
"domNodes": -8331,
"jsHeapTotalBytes": 26275840,
"scriptDurationMs": 593.6680000000001,
"eventListeners": -16460,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-idle",
"durationMs": 16835.53400000005,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 16806.846,
"heapDeltaBytes": 55263144,
"heapUsedBytes": 299600620,
"domNodes": -8333,
"jsHeapTotalBytes": 39645184,
"scriptDurationMs": 710.272,
"eventListeners": -16486,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 16046.425999999996,
"styleRecalcs": 81,
"styleRecalcDurationMs": 21.16699999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 16009.577,
"heapDeltaBytes": -42471156,
"heapUsedBytes": 177388296,
"domNodes": -8331,
"jsHeapTotalBytes": -11296768,
"scriptDurationMs": 914.5509999999999,
"eventListeners": -16458,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.779999999999927,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 15680.050999999934,
"styleRecalcs": 80,
"styleRecalcDurationMs": 21.631999999999984,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 15657.008000000002,
"heapDeltaBytes": -13891636,
"heapUsedBytes": 191543332,
"domNodes": -8331,
"jsHeapTotalBytes": 23830528,
"scriptDurationMs": 930.858,
"eventListeners": -16460,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "workflow-execution",
"durationMs": 464.5269999999755,
"styleRecalcs": 22,
"styleRecalcDurationMs": 25.564999999999998,
"layouts": 4,
"layoutDurationMs": 1.58,
"taskDurationMs": 125.05199999999999,
"heapDeltaBytes": 5364144,
"heapUsedBytes": 56470088,
"domNodes": 192,
"jsHeapTotalBytes": 262144,
"scriptDurationMs": 23.461,
"eventListeners": 71,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999727
},
{
"name": "workflow-execution",
"durationMs": 128.2730000000356,
"styleRecalcs": 12,
"styleRecalcDurationMs": 19.5,
"layouts": 4,
"layoutDurationMs": 1.5739999999999998,
"taskDurationMs": 97.524,
"heapDeltaBytes": 3366744,
"heapUsedBytes": 55268612,
"domNodes": 145,
"jsHeapTotalBytes": 262144,
"scriptDurationMs": 19.377999999999993,
"eventListeners": 37,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
}
]
} |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.ts (1)
91-102:⚠️ Potential issue | 🟠 Major | ⚡ Quick winMove hidden-tab deferral into the visibility watch callback.
When a
ResizeObserverfires while the tab is visible, its measurement is queued on RAF viarafBatch.schedule(). If the tab becomes hidden before that RAF callback executes, the browser will suspend the callback indefinitely. When the tab becomes visible again and RAF resumes,flushPendingMeasurements()will execute withvisibility.value === 'visible', causing the hidden-state check at line 129 to be skipped and the stale measurements to be applied instead of being re-deferred.Move the hidden-state re-deferral logic from lines 127–136 into the visibility watch at lines 91–102 so that pending measurements are immediately shunted to
deferredElementsthe moment the tab is hidden, before RAF suspension takes effect.Suggested fix
watch(visibility, (state) => { - if (state !== 'visible' || deferredElements.size === 0) return + if (state === 'hidden') { + for (const element of pendingMeasurements.keys()) { + deferredElements.add(element) + markElementForFreshMeasurement(element) + resizeObserver.unobserve(element) + } + pendingMeasurements.clear() + return + } + + if (deferredElements.size === 0) return // Re-observe deferred elements to trigger fresh measurements for (const element of deferredElements) { if (element.isConnected) { markElementForFreshMeasurement(element) @@ - // Skip measurements when tab is hidden — bounding rects are unreliable. - // Re-defer the elements so they get a fresh measurement on revisit. - if (visibility.value === 'hidden') { - for (const element of pendingMeasurements.keys()) { - deferredElements.add(element) - markElementForFreshMeasurement(element) - resizeObserver.unobserve(element) - } - pendingMeasurements.clear() - return - } + if (visibility.value === 'hidden') return🤖 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 `@src/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.ts` around lines 91 - 102, The visibility watch must immediately re-defer any pending measurements when the tab becomes hidden to avoid RAF suspension applying stale measurements; modify the callback on visibility (the watch for visibility) so that when state !== 'visible' you iterate any elements currently queued for RAF (the pending/queued set used by rafBatch.schedule()/flushPendingMeasurements()), move them into deferredElements, call markElementForFreshMeasurement(element) for each, and stop/clear the RAF batch/scheduled callback (so flushPendingMeasurements() will not run later), and when visibility becomes 'visible' re-observe deferredElements with resizeObserver and clear deferredElements as the existing code does. Ensure you reference and update the same pending-measurement structures and rafBatch scheduling used by rafBatch.schedule() / flushPendingMeasurements() so there is no race between RAF and visibility changes.
🤖 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.
Outside diff comments:
In `@src/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.ts`:
- Around line 91-102: The visibility watch must immediately re-defer any pending
measurements when the tab becomes hidden to avoid RAF suspension applying stale
measurements; modify the callback on visibility (the watch for visibility) so
that when state !== 'visible' you iterate any elements currently queued for RAF
(the pending/queued set used by rafBatch.schedule()/flushPendingMeasurements()),
move them into deferredElements, call markElementForFreshMeasurement(element)
for each, and stop/clear the RAF batch/scheduled callback (so
flushPendingMeasurements() will not run later), and when visibility becomes
'visible' re-observe deferredElements with resizeObserver and clear
deferredElements as the existing code does. Ensure you reference and update the
same pending-measurement structures and rafBatch scheduling used by
rafBatch.schedule() / flushPendingMeasurements() so there is no race between RAF
and visibility changes.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 064ce991-3777-4739-8bb5-55ef07d46caa
📒 Files selected for processing (2)
src/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.test.tssrc/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.ts
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## main #12299 +/- ##
===========================================
- Coverage 74.67% 59.58% -15.10%
===========================================
Files 1526 1412 -114
Lines 95359 71900 -23459
Branches 27134 19029 -8105
===========================================
- Hits 71212 42839 -28373
- Misses 23285 28588 +5303
+ Partials 862 473 -389
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 1022 files with indirect coverage changes 🚀 New features to boost your workflow:
|
Address CodeRabbit review: between an RO callback and its scheduled RAF flush, the tab can be hidden. The browser then suspends RAF until the tab is shown again, at which point flushPendingMeasurements() runs with visibility === 'visible' and writes stale bounds. Move the hidden-state re-deferral into the visibility watch so pending measurements are immediately shunted to deferredElements / unobserved the moment the tab is hidden, and cancel the scheduled RAF. The flush path keeps a defensive hidden check for any RO fire that races a flip back to visible.
|
@coderabbitai Addressed in Kept a defensive Added a regression test ( |
PR Created by the Glary-Bot Agent
Summary
When the bottom panel opens (or any container animates resizing the canvas), Vue workflow nodes can briefly flicker / appear displaced before the view settles. Repro: open the console bottom panel after a job completes — the canvas goes blank/dark for ~1-2s while the splitter animates.
Root cause: the shared
ResizeObserverinuseVueNodeResizeTracking.tsruns its callback synchronously on every fire. During an animated splitter drag the RO fires multiple times per second, each time computing positions (sometimes viaclientPosToCanvasPos, which readslgCanvas.ds.offset/scale) and immediately writinglayoutStore.batchUpdateNodeBounds(...). If the canvas RO hasn't yet updateddsfor the new container size, the conversion is stale.Change
Mirror the pattern already used by the sibling
useSlotElementTracking.ts:borderBoxSizeper element into apendingMeasurementsmap and schedules acreateRafBatchflush.layoutStorereads, position computation, and writes once per frame.widgets-gridsignal entries continue to route directly toscheduleSlotLayoutSync(which is already RAF-batched), preserving single-RAF latency and existinglinearMode/ hidden-tab gating.Tests
Existing 6 tests pass (with a controllable
rafBatchmock so each test flushes synchronously). Added two regression tests:defers layoutStore writes until the next animation frame— RO callback alone does not write; flush triggers the write.coalesces successive resizes for the same node into one write per frame— two RO callbacks for the same element, only one write with the most recent size.Verification
pnpm test:unit -- src/renderer/extensions/vueNodes/composables/→ 16/16 pass (this file + sibling slot tracking)oxfmt,eslint,pnpm typecheck(via pre-commit) cleanNote
This is one of three planned PRs that work together to fix the bottom-panel/node-displacement bug. The other two will: (2) RAF-batch the canvas-element
useResizeObserverinapp.tsto stop link redraw flicker during splitter drags, and (3) add an explicitbottomPanelVisiblewatcher as belt-and-braces for the splitter transition.┆Issue is synchronized with this Notion page by Unito