perf: draw nodes as boxes on a renderer-owned canvas when zoomed out - #15031
perf: draw nodes as boxes on a renderer-owned canvas when zoomed out#15031benjcooley wants to merge 10 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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 |
|
✅ All contributors have signed the CLA. Thank you! This PR is ready to be merged. |
🎨 Storybook: ✅ Built — View Storybook🎭 Playwright: ✅ 1831 passed, 0 failed · 4 flaky📊 Browser Reports
📦 Bundle: 8.87 MB gzipDetailsSummary
Category Glance App Entry Points — 3.71 kBMain entry bundles and manifests
Status: 1 added Graph Workspace — 1.39 MBGraph editor runtime, canvas, workflow orchestration
Status: 3 added Views & Navigation — 124 kBTop-level views, pages, and routed surfaces
Status: 17 added Panels & Settings — 566 kBConfiguration panels, inspectors, and settings screens
Status: 26 added User & Accounts — 27.5 kBAuthentication, profile, and account management bundles
Status: 11 added Editors & Dialogs — 125 kBModals, dialogs, drawers, and in-app editors
Status: 8 added UI Components — 67.1 kBReusable component library chunks
Status: 14 added Data & Services — 3.53 MBStores, services, APIs, and repositories
Status: 17 added Utilities & Hooks — 549 kBHelpers, composables, and utility bundles
Status: 37 added Vendor & Third-Party — 16.8 MBExternal libraries and shared vendor chunks
Status: 18 added Other — 14.2 MBBundles that do not match a named category
Status: 286 added ⚡ Performance Report
Absolute values
Raw data{
"timestamp": "2026-08-19T17:31:11.563Z",
"gitSha": "f6f6076c33546bd3bd4dd11596834ff5435d268e",
"branch": "benjcooley/vue-nodes-lod-boxes",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2052.099999999996,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.893000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 631.919,
"heapDeltaBytes": 2194712,
"heapUsedBytes": 74241828,
"domNodes": 16,
"jsHeapTotalBytes": 17825792,
"scriptDurationMs": 11.180999999999997,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-idle",
"durationMs": 2034.6450000000686,
"styleRecalcs": 8,
"styleRecalcDurationMs": 8.100999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 497.47,
"heapDeltaBytes": 1597212,
"heapUsedBytes": 73456252,
"domNodes": 16,
"jsHeapTotalBytes": 16515072,
"scriptDurationMs": 8.847000000000001,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1830.5199999999786,
"styleRecalcs": 72,
"styleRecalcDurationMs": 36.609,
"layouts": 12,
"layoutDurationMs": 3.4410000000000003,
"taskDurationMs": 909.411,
"heapDeltaBytes": -3474748,
"heapUsedBytes": 68617560,
"domNodes": 55,
"jsHeapTotalBytes": 19136512,
"scriptDurationMs": 120.34500000000001,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1870.632999999998,
"styleRecalcs": 75,
"styleRecalcDurationMs": 38.523,
"layouts": 12,
"layoutDurationMs": 3.382,
"taskDurationMs": 887.918,
"heapDeltaBytes": -3068112,
"heapUsedBytes": 69005956,
"domNodes": 57,
"jsHeapTotalBytes": 16777216,
"scriptDurationMs": 118.566,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1720.3869999999597,
"styleRecalcs": 33,
"styleRecalcDurationMs": 16.691000000000003,
"layouts": 6,
"layoutDurationMs": 0.5770000000000001,
"taskDurationMs": 365.554,
"heapDeltaBytes": 5497620,
"heapUsedBytes": 77501656,
"domNodes": 78,
"jsHeapTotalBytes": 16515072,
"scriptDurationMs": 8.948,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1753.8729999999987,
"styleRecalcs": 32,
"styleRecalcDurationMs": 18.678,
"layouts": 6,
"layoutDurationMs": 0.5529999999999999,
"taskDurationMs": 417.12999999999994,
"heapDeltaBytes": 5197012,
"heapUsedBytes": 77194332,
"domNodes": 78,
"jsHeapTotalBytes": 17825792,
"scriptDurationMs": 12.158000000000001,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "dom-widget-clipping",
"durationMs": 582.3300000000131,
"styleRecalcs": 11,
"styleRecalcDurationMs": 7.723000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 388.622,
"heapDeltaBytes": -14543968,
"heapUsedBytes": 57391064,
"domNodes": 18,
"jsHeapTotalBytes": 18350080,
"scriptDurationMs": 58.325,
"eventListeners": 0,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "dom-widget-clipping",
"durationMs": 609.6089999999776,
"styleRecalcs": 10,
"styleRecalcDurationMs": 7.348999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 403.08299999999997,
"heapDeltaBytes": -14359008,
"heapUsedBytes": 62392156,
"domNodes": 16,
"jsHeapTotalBytes": 17932288,
"scriptDurationMs": 68.223,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-idle",
"durationMs": 2034.4149999999672,
"styleRecalcs": 8,
"styleRecalcDurationMs": 8.288000000000004,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 596.792,
"heapDeltaBytes": 20995668,
"heapUsedBytes": 86284232,
"domNodes": 16,
"jsHeapTotalBytes": 2883584,
"scriptDurationMs": 17.091999999999995,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-idle",
"durationMs": 2005.749000000037,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.851999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 664.027,
"heapDeltaBytes": 20985692,
"heapUsedBytes": 86895624,
"domNodes": 16,
"jsHeapTotalBytes": 2359296,
"scriptDurationMs": 19.820999999999998,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2177.408000000014,
"styleRecalcs": 68,
"styleRecalcDurationMs": 13.808000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1177.266,
"heapDeltaBytes": 19313000,
"heapUsedBytes": 85314384,
"domNodes": 14,
"jsHeapTotalBytes": 3862528,
"scriptDurationMs": 344.77200000000005,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-pan",
"durationMs": 2329.8039999999673,
"styleRecalcs": 66,
"styleRecalcDurationMs": 12.187999999999997,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1286.958,
"heapDeltaBytes": 18540324,
"heapUsedBytes": 85249836,
"domNodes": 8,
"jsHeapTotalBytes": 4386816,
"scriptDurationMs": 383.047,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-zoom",
"durationMs": 3131.7280000000096,
"styleRecalcs": 65,
"styleRecalcDurationMs": 14.247,
"layouts": 60,
"layoutDurationMs": 7.034000000000001,
"taskDurationMs": 1357.006,
"heapDeltaBytes": 19022248,
"heapUsedBytes": 86875900,
"domNodes": 12,
"jsHeapTotalBytes": 3670016,
"scriptDurationMs": 408.80300000000005,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-zoom",
"durationMs": 3197.848000000022,
"styleRecalcs": 64,
"styleRecalcDurationMs": 14.089,
"layouts": 60,
"layoutDurationMs": 7.2490000000000006,
"taskDurationMs": 1344.54,
"heapDeltaBytes": 16979532,
"heapUsedBytes": 83823816,
"domNodes": 10,
"jsHeapTotalBytes": 3407872,
"scriptDurationMs": 387.61699999999996,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "legacy-node-drag",
"durationMs": 2274.169000000029,
"styleRecalcs": 46,
"styleRecalcDurationMs": 9.366,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1418.1979999999999,
"heapDeltaBytes": 10154060,
"heapUsedBytes": 78175236,
"domNodes": 12,
"jsHeapTotalBytes": 4349952,
"scriptDurationMs": 453.085,
"eventListeners": 184,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "legacy-node-drag",
"durationMs": 2736.0120000000734,
"styleRecalcs": 44,
"styleRecalcDurationMs": 8.713,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1575.741,
"heapDeltaBytes": 11320740,
"heapUsedBytes": 79866800,
"domNodes": 8,
"jsHeapTotalBytes": 6455296,
"scriptDurationMs": 496.53000000000003,
"eventListeners": 186,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 1993.5909999999808,
"styleRecalcs": 8,
"styleRecalcDurationMs": 6.752000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 535.582,
"heapDeltaBytes": 21011468,
"heapUsedBytes": 86476724,
"domNodes": 16,
"jsHeapTotalBytes": 2883584,
"scriptDurationMs": 13.787,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "minimap-idle",
"durationMs": 2003.783999999996,
"styleRecalcs": 7,
"styleRecalcDurationMs": 6.541000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 549.519,
"heapDeltaBytes": 20837232,
"heapUsedBytes": 87349108,
"domNodes": 14,
"jsHeapTotalBytes": 3932160,
"scriptDurationMs": 16.617,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 598.352000000034,
"styleRecalcs": 46,
"styleRecalcDurationMs": 10.047999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 404.39300000000003,
"heapDeltaBytes": -14552060,
"heapUsedBytes": 57506596,
"domNodes": 18,
"jsHeapTotalBytes": 18350080,
"scriptDurationMs": 119.94599999999998,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 625.165999999922,
"styleRecalcs": 47,
"styleRecalcDurationMs": 11.577,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 428.421,
"heapDeltaBytes": -13290404,
"heapUsedBytes": 58793192,
"domNodes": 20,
"jsHeapTotalBytes": 16777216,
"scriptDurationMs": 124.24799999999999,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-idle",
"durationMs": 2006.7650000000299,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.397999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 484.083,
"heapDeltaBytes": 2591252,
"heapUsedBytes": 74714932,
"domNodes": 20,
"jsHeapTotalBytes": 17301504,
"scriptDurationMs": 9.095,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-idle",
"durationMs": 2038.8719999999694,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.263,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 509.73100000000005,
"heapDeltaBytes": 2391916,
"heapUsedBytes": 74551048,
"domNodes": 20,
"jsHeapTotalBytes": 16515072,
"scriptDurationMs": 8.734000000000002,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1689.6229999999832,
"styleRecalcs": 75,
"styleRecalcDurationMs": 39.588,
"layouts": 16,
"layoutDurationMs": 4.994999999999999,
"taskDurationMs": 807.903,
"heapDeltaBytes": -6648332,
"heapUsedBytes": 65456632,
"domNodes": 61,
"jsHeapTotalBytes": 18087936,
"scriptDurationMs": 93.539,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1709.594999999922,
"styleRecalcs": 75,
"styleRecalcDurationMs": 36.725,
"layouts": 16,
"layoutDurationMs": 4.4190000000000005,
"taskDurationMs": 822.278,
"heapDeltaBytes": -6909068,
"heapUsedBytes": 65152928,
"domNodes": 61,
"jsHeapTotalBytes": 17301504,
"scriptDurationMs": 95.273,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "viewport-pan-sweep",
"durationMs": 8341.731999999978,
"styleRecalcs": 250,
"styleRecalcDurationMs": 36.175,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4151.586,
"heapDeltaBytes": 24950096,
"heapUsedBytes": 90364960,
"domNodes": 16,
"jsHeapTotalBytes": 6221824,
"scriptDurationMs": 1070.143,
"eventListeners": 22,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8280.239000000052,
"styleRecalcs": 250,
"styleRecalcDurationMs": 36.687999999999995,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4091.8179999999998,
"heapDeltaBytes": 9919688,
"heapUsedBytes": 75352324,
"domNodes": -138,
"jsHeapTotalBytes": 2523136,
"scriptDurationMs": 1058.574,
"eventListeners": -114,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 4064.341000000013,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 2701.0170000000003,
"heapDeltaBytes": -8996636,
"heapUsedBytes": 97605140,
"domNodes": -1605,
"jsHeapTotalBytes": 8097792,
"scriptDurationMs": 32.98699999999999,
"eventListeners": -1784,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.80000000000109
},
{
"name": "vue-large-graph-idle",
"durationMs": 4086.496000000011,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 2880.689,
"heapDeltaBytes": -9409900,
"heapUsedBytes": 97619652,
"domNodes": -1605,
"jsHeapTotalBytes": 8097792,
"scriptDurationMs": 33.566999999999986,
"eventListeners": -1784,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-pan",
"durationMs": 5202.631999999994,
"styleRecalcs": 65,
"styleRecalcDurationMs": 17.953999999999997,
"layouts": 3,
"layoutDurationMs": 0.37000000000000227,
"taskDurationMs": 3878.3880000000004,
"heapDeltaBytes": -20844940,
"heapUsedBytes": 89207720,
"domNodes": -1941,
"jsHeapTotalBytes": 2813952,
"scriptDurationMs": 564.548,
"eventListeners": -1814,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 5188.743999999929,
"styleRecalcs": 66,
"styleRecalcDurationMs": 17.986,
"layouts": 3,
"layoutDurationMs": 0.3759999999999944,
"taskDurationMs": 3949.4630000000006,
"heapDeltaBytes": 25212040,
"heapUsedBytes": 115950388,
"domNodes": -1625,
"jsHeapTotalBytes": -327680,
"scriptDurationMs": 583.233,
"eventListeners": -1780,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-zoom-culling",
"durationMs": 3437.621999999976,
"styleRecalcs": 45,
"styleRecalcDurationMs": 44.787000000000006,
"layouts": 42,
"layoutDurationMs": 20.594999999999995,
"taskDurationMs": 1520.6139999999998,
"heapDeltaBytes": 22973972,
"heapUsedBytes": 118918728,
"domNodes": 1644,
"jsHeapTotalBytes": 3670016,
"scriptDurationMs": 427.54400000000004,
"eventListeners": 290,
"totalBlockingTimeMs": 27,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-zoom-culling",
"durationMs": 3560.2480000000014,
"styleRecalcs": 44,
"styleRecalcDurationMs": 40.69499999999999,
"layouts": 41,
"layoutDurationMs": 20.145999999999997,
"taskDurationMs": 1614.4129999999998,
"heapDeltaBytes": 23144744,
"heapUsedBytes": 116888676,
"domNodes": 1632,
"jsHeapTotalBytes": 3670016,
"scriptDurationMs": 469.07300000000004,
"eventListeners": 292,
"totalBlockingTimeMs": 29,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "workflow-execution",
"durationMs": 125.18899999997757,
"styleRecalcs": 10,
"styleRecalcDurationMs": 19.165000000000003,
"layouts": 4,
"layoutDurationMs": 1.538,
"taskDurationMs": 92.124,
"heapDeltaBytes": 3337188,
"heapUsedBytes": 74335404,
"domNodes": 140,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 6.690000000000001,
"eventListeners": 49,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "workflow-execution",
"durationMs": 475.6560000000718,
"styleRecalcs": 18,
"styleRecalcDurationMs": 23.015999999999995,
"layouts": 3,
"layoutDurationMs": 1.5939999999999999,
"taskDurationMs": 119.27300000000002,
"heapDeltaBytes": 5288508,
"heapUsedBytes": 76278204,
"domNodes": 147,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 7.667,
"eventListeners": 99,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
}
]
} |
|
I have read and agree to the Contributor License Agreement |
Codecov Report❌ Patch coverage is @@ Coverage Diff @@
## benjcooley/vue-nodes-simple-keepalive #15031 +/- ##
=======================================================================
Coverage 81.37% 81.38%
=======================================================================
Files 1886 1890 +4
Lines 115278 115601 +323
Branches 35771 35858 +87
=======================================================================
+ Hits 93812 94080 +268
- Misses 21008 21063 +55
Partials 458 458
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 1 file with indirect coverage changes 🚀 New features to boost your workflow:
|
570eeb8 to
e28d5f2
Compare
e28d5f2 to
bc7e1ee
Compare
bc7e1ee to
c69cd94
Compare
|
Updating Playwright Expectations |
|
@christian-byrne Right — and that assumption mostly holds because the persistent state doesn't live in the components: positions/sizes are in layoutStore, selection in canvasStore, z-index in layoutStore, execution/preview state in their stores, so unmount loses nothing there. The known exceptions are element-local DOM widget state (media playback position, IME composition, focus) which survives detach only partially, and the DOM-measured slot layouts, which are deleted on unmount by useSlotElementTracking — links then fall back to computed geometry. That last one is tracked as FE-1557 (preserve slot layouts across cull), which also removes the fallback's ~11% pan-time cost. Selected nodes are never culled, so an in-progress drag/resize can't be torn down mid-interaction. |
f5d3c2a to
34a2db0
Compare
34a2db0 to
31f27d9
Compare
🔗 Website PreviewWebsite Preview: https://comfy-website-preview-pr-15031.vercel.app This commit: https://website-frontend-2zcotd9o6-comfyui.vercel.app Last updated: 2026-08-18T18:54:21Z for |
31f27d9 to
0391723
Compare
|
Thanks for adding this, and it is a good ADR. The Decision section is precise, and recording that a second spatial index and mount scheduler were rejected is exactly the thing DrJKL's review was asking for. I checked it against the Slack thread specifically for what a future maintainer or agent would need and cannot get from the code. Nine items came up: four partly covered, five absent. One of them I would fix before merge because it is not a gap but a wrong impression. The one that is actively misleadingEvidence says Something I found while checking, which is worse than a documentation gapThe 158-line That leaves this ADR's Evidence section, including the 8.3 versus 8.6 ms/frame comparison, with no committed source behind it. Either restore them or point Evidence at the PlacementThe ADR is only on this branch, but items 1 through 6 below are about the KeepAlive decision, which is #15405. #15031 is stacked on #15405, so if the base lands alone, which is the likely order, the decision ships and its rationale does not. I would move the file and its README index row down to #15405 unchanged, keep it as one ADR covering both halves, and add a line to Status noting which PR delivers which half. The nine itemsVerdicts: item 1 partial, 2 missing, 3 partial and misleading, 4 partial, 5 missing, 6 missing, 7 missing, 8 missing, 9 missing. Drafted below in the ADR's own voice and section structure so it is paste-ready. Take or leave any of it; the wording is not the point, the content is. All of the following is wrapped to the ADR's existing column width, uses the Item 1. Replace the "Destroy offscreen components" alternativeReplace the whole subsection body: ### Destroy offscreen components
Rejected as the default. Interaction performance landed in the same tier as
`KeepAlive`, so maintainability decided this and not speed. Unmounting fires
`mounted` and `unmounted` on every viewport crossing, which node components and
extensions already treat as create and destroy, and it discards
component-local state. `KeepAlive` fires `activated` and `deactivated` instead,
a separate lifecycle that existing code does not overload, and the instance
keeps its state across the transition.Item 2. New alternative, insert after "Keep every Vue node attached"### Virtualize the node list and mount only visible nodes
Built, reviewed, and abandoned as
[#15030](https://github.com/Comfy-Org/ComfyUI_frontend/pull/15030), roughly
2700 lines added. Virtualization was going to be the default, and the default
was deliberately reversed once `KeepAlive` measured the same and turned out to
be the smaller lifecycle change. The implementation was then removed rather
than kept behind a setting, so there is one attachment path to reason about
instead of two. The approach works and is not disqualified; it was dropped
because it costs more machinery than the current activation threshold
justifies. Anyone revisiting it should start from that PR rather than rebuild
it.Note the wording: this rejects the implementation, not the virtualization Item 3. Replace the first Evidence paragraphReplace from "On the 245-node workflow" through "primary gain.": On the 245-node workflow used for the original performance investigation,
removing offscreen DOM improved scripted interactions on Chromium, Firefox, and
WebKit. Between the two retention strategies, `KeepAlive` is slower than
destructive unmounting on Chrome specifically and close to it on the other
browsers, so active DOM size and not the retention strategy is the primary
gain. Chrome is both where this design is weakest and where nearly all users
are, while most of the team dailies non-Chrome browsers. Dogfooding is
therefore biased away from the worst case, and Chrome needs explicit attention
in any follow-up measurement.Item 3, second half. Add to Consequences / Negative- `KeepAlive` is slower than unmounting on Chrome, which is where almost all
users are and where the team dogfoods least.Item 4. Replace the first Negative bullet- `KeepAlive` retains component memory for detached nodes. The cost grows with
graph size and has not been measured, so the magnitude is unknown.Item 5. Append to the Evidence paragraph that quotes 8.3 vs 8.6 ms/frameRead that comparison narrowly. Nodes 2.0 is now close to LiteGraph on these
workflows rather than ahead of it, and some of this work also made LiteGraph
faster, so a before and after against an older `main` overstates the gain
attributable to viewport retention. Compare Nodes 2.0 against LiteGraph on the
same build.Item 9. Append to Evidence, after the behavioral coverage paragraphThe cross-browser comparison behind these statements came from a generated
benchmark report the team chose not to publish, so no numbers from it are
quoted here. The committed and re-runnable source is the `@perf` suite in
`browser_tests/tests/performance.spec.ts` with
`browser_tests/fixtures/utils/perfReporter.ts`. Anything this ADR asserts about
performance should be reproducible from that suite; if it is not, treat it as
unsourced.Items 6, 7, 8. New Notes section, appended at the end of the ADRThe house template puts a ## Notes
Agreed direction for follow-up performance work: keep this virtualization
model and fix where it breaks down rather than reverting to attaching every
node. The available tools are overscan, hysteresis on membership transitions,
buffered visibility regions, and retained rendering. Prefer a buffered
visibility region owned by the renderer over a per-node `isInViewport` flag; a
per-node predicate puts a reactive boundary on every node and scatters the
overscan and hysteresis tuning that a single region keeps in one place.
Viewport retention and the low-quality box layer interact. Render-on-pan lag is
more noticeable when the level-of-detail change is not also present, so the two
are best evaluated together. They may land as separate PRs, and if retention
ships first then panning will feel worse than the measurements here suggest.
A risk to watch: profiling does not always survive contact with real users,
real workflows, and real hardware. Treat the scripted measurements as a
starting point and monitor real-world experience reports after release rather
than closing this out on the numbers alone. |
christian-byrne
left a comment
There was a problem hiding this comment.
Verified all 18 threads against the code at head rather than against the replies. 10 confirmed, 4 partial, 1 not fixed, 3 no longer applicable. I have resolved the confirmed ones and replied in place on the rest naming exactly what is left, so the threads should tell you what to do without re-reading this.
The one I owe you an apology on is now closed correctly. The double title-height offset was my error, and the fix is right: I derived it end to end and the box span matches the node span exactly, with widgets on the body-relative rect and slots on absolute getSlotPosition, all three in register.
One reply does not hold up and I have left that thread open. On the culling-index fail-open, the reply says layoutStore owns that handling. It does not: grep -rn unpositioned src/ is empty, SpatialIndexManager.insert discards QuadTree.insert's boolean, and the root is still the fixed +/-10000 box. So a node outside it is absent from every query, attached by the keepalive fail-open and never drawn as a box, i.e. invisible below the threshold. Your own #15254 is the root cause; this PR needs either to depend on that fix or reinstate a fail-open list on the read path.
Three threads are now moot for a reason worth its own attention: the measurement doc and all of scripts/perf/ are gone from every branch, including the abandoned one. I raised that on the ADR comment because it leaves the ADR's own numbers unsourced.
One new finding inline, on the same line as a thread I just closed.
christian-byrne
left a comment
There was a problem hiding this comment.
Re-review after the rebase onto #15405, head 0391723f78. Status on the five open threads, plus one new blocking finding the rebase introduced.
The rebase reverted the SpatialIndexManager hardening
This is the important one, and it is a regression rather than a pre-existing gap.
On #15030 you closed DrJKL's "second QuadTree" blocker by deleting nodeCullingIndex.ts and hardening the canonical index. That branch's SpatialIndex.ts has unindexableEntries, an isIndexable() check, if (!indexed) this.rebuild(), and batched-growth handling — verified at origin/benjcooley/vue-nodes-viewport-culling:src/renderer/core/spatial/SpatialIndex.ts:37-98.
None of it is on this branch. SpatialIndex.ts is untouched relative to #15405, and insert() is back to:
insert(nodeId: TId, bounds: Bounds): void {
this.quadTree.insert(String(nodeId), bounds, nodeId)
this.invalidateCache()
}QuadTree.insert returns false when !this.contains(item.bounds) (QuadTree.ts:52-54), the index is constructed with no explicit bounds (layoutStore.ts:230) so it uses DEFAULT_BOUNDS = ±10000, and that boolean is discarded.
This matters more here than it did on #15030. Below the LOD threshold Vue nodes are detached, so getNodeBoxes is the only thing drawing a node — and it draws exactly what queryNodesInBounds returns. A node beyond ±10000 fails insert silently, is absent from the query, and is therefore invisible when zoomed out: not mounted, not drawn. ±10000 is reachable in a real workflow.
This also re-opens thread 3 in substance. I left it open saying "no fail-open list exists anywhere in the tree"; the reply pointed at layoutStore. On #15030 that reply became true. On this branch it is false again, and the relocation to queryNodesInBounds means the two-readers-disagree framing no longer applies — now there is one reader and it silently omits.
Cheapest correct fix is to bring SpatialIndex.ts forward from the #15030 branch. It is self-contained and already reviewed.
Thread status
| Thread | State |
|---|---|
nodeCullingIndex.queryEntries omits unpositioned |
Superseded, hazard live — file deleted, but see above. Suggest re-scoping this thread to the SpatialIndex regression rather than closing it. |
useLowQualityRendering header vs HYSTERESIS |
Unchanged. Lines 5-6 still say both renderers "simplify at exactly the same zoom and the same setting controls both"; HYSTERESIS = 0.15 at :28 is still applied at :75. Still no composable-level DPR test. |
nodeBoxRenderer colour batching |
Unchanged, and the second half is now confirmed — see inline. |
getBoxes allocation / O(all) |
Half closed. queryNodesInBounds + viewport expansion landed; O(visible) is right and the title offset is correctly applied once via includeNodeTitleInBounds. Allocation half still open, non-blocking. |
| Cold-start re-arm | Not re-verified this round; leaving as-is. |
Direction remains right and the LOD work itself is clean. The blocking item is purely the lost SpatialIndex hardening — a rebase artifact, not a design choice.
0391723 to
c8aa094
Compare
christian-byrne
left a comment
There was a problem hiding this comment.
Approving. All threads resolved, CI 35 pass / 0 fail at 1c303b03d6. Verified each fix at head.
The blocking one — the SpatialIndexManager hardening lost in the rebase — is fully restored: unindexableEntries (:37), the isIndexable branch (:54-58), if (!indexed) this.rebuild() (:67), the batched rebuildRequired path (:83-105), and query concatenating unindexable entries so it fails open (:140-141). SpatialIndex.test.ts:6-17 inserts at x: 50_000, outside the original ±10000 root, and asserts the query returns it. That test is the part that matters — it's what stops this being lost to a rebase a third time.
The setFill fix is better than the read-back I proposed. Assigning the known-good fallback before attempting the desired value means a rejected colour degrades to the default rather than inheriting the previous box's, and reading currentColor back off ctx.fillStyle makes cache/context divergence structurally impossible instead of merely unlikely. It also closed the slot-colour input for free, since those now route through the same fallback.
Also verified: nodeBoxCache removes the per-frame NodeLayout + slot/widget materialisation on the moving path; the file header now describes the shared threshold plus the deliberate Vue-only hysteresis instead of claiming identical transition zooms; and the cold-start re-arm case is unreachable now that the re-arm is gated on wasLow && !low, so the immediate: true run at cold start can't trigger it.
One condition, carried from #15405
Land these together, or this one immediately behind #15405. #15405 alone has no bound on the activation set at low zoom; this PR is what supplies it, by switching the template off activeNodeIds below the LOD threshold. I accepted the decline to add a redundant cap in #15405 on that basis, so the sequencing is load-bearing.
ADR 0016 lives only on this PR, which is the second reason the two shouldn't separate — merging #15405 first puts the KeepAlive architecture in main undocumented. The six Slack items the ADR is still missing are listed on #15405; they apply to this file since it's where the ADR lives.
Summary
Below the readable-text zoom threshold, Nodes 2.0 replaces ordinary Vue node DOM with renderer-owned canvas boxes, avoiding mass DOM activation during zoom-out.
Changes
Review Focus
This is one LOD layer stacked directly on #15405. GraphCanvas derives the attached ID set and plain box data; threshold policy and drawing remain separate modules. Graph membership, geometry, serialization, and node sizes are unchanged.
The overlay is idle unless camera, layout, geometry, theme, DPR, panel size, or node content changes. Camera movement queries only visible IDs and reuses cached box, slot, and widget objects until their source versions change.
Validation