Skip to content

docs: write down how to model a feature's state, and lint the checkable part - #14472

Merged
christian-byrne merged 1 commit into
mainfrom
christian/frontend-state-guidance
Aug 18, 2026
Merged

docs: write down how to model a feature's state, and lint the checkable part#14472
christian-byrne merged 1 commit into
mainfrom
christian/frontend-state-guidance

Conversation

@christian-byrne

Copy link
Copy Markdown
Contributor

Writes down the state-modelling conventions we keep re-explaining in review, and
adds one lint rule for the part that is mechanically checkable.

Why

Reviews of anything with a beginning and an end — a wizard, an upload, an
onboarding flow — keep converging on the same handful of points: several
independent booleans that must agree, values stored that could be derived, and
effects used to keep two pieces of internal state in sync rather than to talk to
something outside the app. Each time it gets re-argued from scratch on the PR,
which is slow for the reviewer and unpleasant for the author, who reasonably
asks why nobody said so earlier.

docs/guidance/state-and-effects.md is glob-loaded on src/**/*.ts and
src/**/*.vue, so it applies automatically rather than needing to be found.

What it says

One state value as a discriminated union instead of N booleans, named events
rather than assignment scattered across call sites, a pure transition function so
one place owns the rules, computed for anything derivable, and effects reserved
for synchronising outward. Plus the Vue mapping — state to Pinia, transition to a
pure function, derivation to computed, synchronisation to watch.

It deliberately does not recommend a state-machine library. There is no
XState or createMachine precedent anywhere in src/, and a hand-rolled union
with a pure reduce gets the same guarantees without the dependency.

The lint rule

no-restricted-syntax flagging getBoundingClientRect, getComputedStyle and
querySelector* inside a computed. A derivation that measures the DOM runs a
layout read on every recompute and cannot be unit-tested without a browser.

It ships at warn, not error, because there are exactly four pre-existing
instances and this PR does not fix them:

File Count
src/components/maskeditor/BrushCursor.vue 2
src/components/breadcrumb/SubgraphBreadcrumb.vue 1
src/components/topbar/WorkflowTabs.vue 1

Promote to error once those derive from stores instead. I verified the count
against main at the current head rather than trusting the number I first wrote.

Scope

Docs and lint config only — no runtime code, no behaviour change.

🤖 Generated with Claude Code

This advice has been given per-review rather than written down, most recently
across the first-run onboarding tour stack. Putting it where the glob-loaded
guidance lives so it stops being re-derived in review comments.

The shape: one state as a discriminated union rather than several booleans that
must agree; named events and a pure transition rather than assignments spread
across callers; async orchestration in commands rather than chains of effects
reacting to each other; derive anything derivable; and reserve effects for
synchronising outward.

Also two things that are easy to discover the hard way, so they are written
down: layoutStore holds a node's body box, not the box the user sees, and node
ids are graph-local so one held across a graph change names something else.

Adds a lint rule for the mechanically checkable part - measuring or querying
the DOM inside a computed, where every recompute becomes a layout read. It is
'warn' rather than 'error' because four pre-existing instances remain
(BrushCursor, WorkflowTabs, SubgraphBreadcrumb); it should be promoted once
those derive from stores instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@christian-byrne
christian-byrne requested a review from a team July 31, 2026 08:05
@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Jul 31, 2026
@dosubot

dosubot Bot commented Jul 31, 2026

Copy link
Copy Markdown

📄 Knowledge review

Dosu skipped reviewing this PR because your organization has used its 200 included credits for the month. Your usage will reset on 2026-08-01. To have Dosu review this PR before then, ask your organization admin to upgrade to a pro account.


Leave Feedback Ask Dosu about ComfyUI_frontend Add Dosu to your team

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown

🎭 Playwright: ✅ 1772 passed, 0 failed · 1 flaky

📊 Browser Reports
  • chromium: View Report (✅ 1751 / ❌ 0 / ⚠️ 1 / ⏭️ 5)
  • chromium-2x: View Report (✅ 2 / ❌ 0 / ⚠️ 0 / ⏭️ 0)
  • chromium-0.5x: View Report (✅ 1 / ❌ 0 / ⚠️ 0 / ⏭️ 0)
  • mobile-chrome: View Report (✅ 18 / ❌ 0 / ⚠️ 0 / ⏭️ 0)

📦 Bundle: 8.19 MB gzip ⚪ 0 B

Details

Summary

  • Raw size: 34.4 MB baseline 34.4 MB — ⚪ 0 B
  • Gzip: 8.19 MB baseline 8.19 MB — ⚪ 0 B
  • Brotli: 5.67 MB baseline 5.67 MB — ⚪ 0 B
  • Bundles: 426 current • 426 baseline

Category Glance
Vendor & Third-Party ⚪ 0 B (15.7 MB) · Other ⚪ 0 B (12.7 MB) · Data & Services ⚪ 0 B (3.44 MB) · Graph Workspace ⚪ 0 B (1.29 MB) · Panels & Settings ⚪ 0 B (551 kB) · Utilities & Hooks ⚪ 0 B (386 kB) · + 5 more

App Entry Points — 3.62 kB (baseline 3.62 kB) • ⚪ 0 B

Main entry bundles and manifests

Status: 1 unchanged

Graph Workspace — 1.29 MB (baseline 1.29 MB) • ⚪ 0 B

Graph editor runtime, canvas, workflow orchestration

Status: 2 unchanged

Views & Navigation — 111 kB (baseline 111 kB) • ⚪ 0 B

Top-level views, pages, and routed surfaces

Status: 16 unchanged

Panels & Settings — 551 kB (baseline 551 kB) • ⚪ 0 B

Configuration panels, inspectors, and settings screens

Status: 26 unchanged

User & Accounts — 28.7 kB (baseline 28.7 kB) • ⚪ 0 B

Authentication, profile, and account management bundles

Status: 10 unchanged

Editors & Dialogs — 124 kB (baseline 124 kB) • ⚪ 0 B

Modals, dialogs, drawers, and in-app editors

Status: 7 unchanged

UI Components — 70 kB (baseline 70 kB) • ⚪ 0 B

Reusable component library chunks

Status: 15 unchanged

Data & Services — 3.44 MB (baseline 3.44 MB) • ⚪ 0 B

Stores, services, APIs, and repositories

Status: 17 unchanged

Utilities & Hooks — 386 kB (baseline 386 kB) • ⚪ 0 B

Helpers, composables, and utility bundles

Status: 36 unchanged

Vendor & Third-Party — 15.7 MB (baseline 15.7 MB) • ⚪ 0 B

External libraries and shared vendor chunks

Status: 16 unchanged

Other — 12.7 MB (baseline 12.7 MB) • ⚪ 0 B

Bundles that do not match a named category

Status: 280 unchanged

⚡ Performance Report

canvas-idle: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 69.2 MB heap
canvas-mouse-sweep: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 49.6 MB heap
canvas-zoom-sweep: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 72.3 MB heap
dom-widget-clipping: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 53.1 MB heap
large-graph-idle: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 64.3 MB heap
large-graph-pan: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 61.4 MB heap
large-graph-zoom: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 70.2 MB heap
minimap-idle: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 65.4 MB heap
subgraph-dom-widget-clipping: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 54.0 MB heap
subgraph-idle: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 43.9 MB heap
subgraph-mouse-sweep: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 44.3 MB heap
subgraph-transition-enter: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 119ms TBT · 94.9 MB heap
viewport-pan-sweep: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 66.5 MB heap
vue-large-graph-idle: · 56.3 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 166.3 MB heap
vue-large-graph-pan: · 55.4 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 379ms TBT · 158.6 MB heap
workflow-execution: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 57.7 MB heap

⚠️ 7 regressions detected

Show regressions
Metric Baseline PR (median) Δ Sig
canvas-idle: task duration 452ms 518ms +15% ⚠️ z=4.0
canvas-zoom-sweep: task duration 362ms 395ms +9% ⚠️ z=3.0
large-graph-idle: task duration 657ms 681ms +4% ⚠️ z=2.6
large-graph-pan: task duration 1244ms 1282ms +3% ⚠️ z=4.6
minimap-idle: task duration 650ms 689ms +6% ⚠️ z=3.4
subgraph-dom-widget-clipping: task duration 392ms 421ms +7% ⚠️ z=2.3
subgraph-idle: task duration 467ms 514ms +10% ⚠️ z=4.6
All metrics
Metric Baseline PR (median) Δ Sig
canvas-idle: avg frame time 17ms 17ms +0% z=-0.1
canvas-idle: p95 frame time 17ms 17ms -0%
canvas-idle: layout duration 0ms 0ms +0%
canvas-idle: style recalc duration 9ms 8ms -12% z=-3.5
canvas-idle: layout count 0 0 +0%
canvas-idle: style recalc count 10 9 -10% z=-3.8
canvas-idle: task duration 452ms 518ms +15% ⚠️ z=4.0
canvas-idle: script duration 14ms 20ms +36% z=-2.5
canvas-idle: TBT 0ms 0ms +0%
canvas-idle: heap used 69.5 MB 69.2 MB -0%
canvas-idle: DOM nodes 20 18 -10% z=-3.6
canvas-idle: event listeners 4 4 +0% z=-1.6
canvas-mouse-sweep: avg frame time 17ms 17ms +0% z=-0.9
canvas-mouse-sweep: p95 frame time 17ms 17ms -1%
canvas-mouse-sweep: layout duration 4ms 4ms +13% z=2.0
canvas-mouse-sweep: style recalc duration 37ms 45ms +23% z=0.8
canvas-mouse-sweep: layout count 12 12 +0%
canvas-mouse-sweep: style recalc count 74 77 +4% z=-0.7
canvas-mouse-sweep: task duration 849ms 942ms +11% z=1.3
canvas-mouse-sweep: script duration 118ms 123ms +4% z=-1.9
canvas-mouse-sweep: TBT 0ms 0ms +0%
canvas-mouse-sweep: heap used 52.2 MB 49.6 MB -5%
canvas-mouse-sweep: DOM nodes -277 -280 +1% z=-132.2
canvas-mouse-sweep: event listeners -148 -147 -1% z=-37.3
canvas-zoom-sweep: avg frame time 17ms 17ms +0% z=0.5
canvas-zoom-sweep: p95 frame time 17ms 17ms +0%
canvas-zoom-sweep: layout duration 1ms 1ms +7% z=-0.6
canvas-zoom-sweep: style recalc duration 16ms 17ms +6% z=-1.4
canvas-zoom-sweep: layout count 6 6 +0%
canvas-zoom-sweep: style recalc count 31 32 +2% z=0.5
canvas-zoom-sweep: task duration 362ms 395ms +9% ⚠️ z=3.0
canvas-zoom-sweep: script duration 17ms 21ms +24% z=-2.0
canvas-zoom-sweep: TBT 0ms 0ms +0%
canvas-zoom-sweep: heap used 72.3 MB 72.3 MB -0%
canvas-zoom-sweep: DOM nodes 77 77 -1% z=-3.5
canvas-zoom-sweep: event listeners 19 19 +0% z=-0.9
dom-widget-clipping: avg frame time 17ms 17ms +0% z=0.1
dom-widget-clipping: p95 frame time 17ms 17ms -1%
dom-widget-clipping: layout duration 0ms 0ms +0%
dom-widget-clipping: style recalc duration 8ms 6ms -16% z=-4.3
dom-widget-clipping: layout count 0 0 +0%
dom-widget-clipping: style recalc count 11 10 -9% z=-6.2
dom-widget-clipping: task duration 364ms 386ms +6% z=1.3
dom-widget-clipping: script duration 56ms 58ms +4% z=-2.9
dom-widget-clipping: TBT 0ms 0ms +0%
dom-widget-clipping: heap used 53.2 MB 53.1 MB -0%
dom-widget-clipping: DOM nodes 18 16 -11% z=-4.3
dom-widget-clipping: event listeners 0 0 +0% variance too high
large-graph-idle: avg frame time 17ms 17ms -0% z=-1.0
large-graph-idle: p95 frame time 17ms 17ms -0%
large-graph-idle: layout duration 0ms 0ms +0%
large-graph-idle: style recalc duration 8ms 7ms -7% z=-4.9
large-graph-idle: layout count 0 0 +0%
large-graph-idle: style recalc count 9 8 -11% z=-11.5
large-graph-idle: task duration 657ms 681ms +4% ⚠️ z=2.6
large-graph-idle: script duration 101ms 100ms -1% z=-0.3
large-graph-idle: TBT 0ms 0ms +0%
large-graph-idle: heap used 63.6 MB 64.3 MB +1%
large-graph-idle: DOM nodes -281 -284 +1% z=-341.0
large-graph-idle: event listeners -142 -143 +1% z=-27.7
large-graph-pan: avg frame time 17ms 17ms +0% z=0.3
large-graph-pan: p95 frame time 17ms 17ms +0%
large-graph-pan: layout duration 0ms 0ms +0%
large-graph-pan: style recalc duration 12ms 14ms +16% z=-3.6
large-graph-pan: layout count 0 0 +0%
large-graph-pan: style recalc count 68 68 +0% z=-2.4
large-graph-pan: task duration 1244ms 1282ms +3% ⚠️ z=4.6
large-graph-pan: script duration 424ms 426ms +1% z=0.9
large-graph-pan: TBT 0ms 0ms +0%
large-graph-pan: heap used 62.9 MB 61.4 MB -2%
large-graph-pan: DOM nodes -284 -285 +0% z=-184.3
large-graph-pan: event listeners -144 -143 -1% z=-178.6
large-graph-zoom: avg frame time 17ms 17ms +0%
large-graph-zoom: p95 frame time 17ms 17ms -0%
large-graph-zoom: layout duration 7ms 8ms +17%
large-graph-zoom: style recalc duration 15ms 16ms +8%
large-graph-zoom: layout count 60 60 +0%
large-graph-zoom: style recalc count 66 65 -2%
large-graph-zoom: task duration 1466ms 1508ms +3%
large-graph-zoom: script duration 512ms 529ms +3%
large-graph-zoom: TBT 0ms 0ms +0%
large-graph-zoom: heap used 57.5 MB 70.2 MB +22%
large-graph-zoom: DOM nodes -287 -139 -52%
large-graph-zoom: event listeners -148 -70 -53%
minimap-idle: avg frame time 17ms 17ms +0% z=0.1
minimap-idle: p95 frame time 17ms 17ms +0%
minimap-idle: layout duration 0ms 0ms +0%
minimap-idle: style recalc duration 7ms 7ms -4% z=-3.1
minimap-idle: layout count 0 0 +0%
minimap-idle: style recalc count 8 8 +0% z=-2.3
minimap-idle: task duration 650ms 689ms +6% ⚠️ z=3.4
minimap-idle: script duration 101ms 101ms -0% z=0.3
minimap-idle: TBT 0ms 0ms +0%
minimap-idle: heap used 65.1 MB 65.4 MB +1%
minimap-idle: DOM nodes -282 -283 +0% z=-220.5
minimap-idle: event listeners -144 -144 +0% z=-225.0
subgraph-dom-widget-clipping: avg frame time 17ms 17ms -0% z=-0.9
subgraph-dom-widget-clipping: p95 frame time 17ms 17ms +1%
subgraph-dom-widget-clipping: layout duration 0ms 0ms +0%
subgraph-dom-widget-clipping: style recalc duration 10ms 11ms +18% z=-1.4
subgraph-dom-widget-clipping: layout count 0 0 +0%
subgraph-dom-widget-clipping: style recalc count 47 47 +0% z=-1.6
subgraph-dom-widget-clipping: task duration 392ms 421ms +7% ⚠️ z=2.3
subgraph-dom-widget-clipping: script duration 116ms 123ms +6% z=-0.9
subgraph-dom-widget-clipping: TBT 0ms 0ms +0%
subgraph-dom-widget-clipping: heap used 53.9 MB 54.0 MB +0%
subgraph-dom-widget-clipping: DOM nodes 20 20 +0% z=-1.9
subgraph-dom-widget-clipping: event listeners 6 8 +33% z=-1.4
subgraph-idle: avg frame time 17ms 17ms -0% z=-0.2
subgraph-idle: p95 frame time 17ms 17ms +1%
subgraph-idle: layout duration 0ms 0ms +0%
subgraph-idle: style recalc duration 9ms 8ms -2% z=-2.5
subgraph-idle: layout count 0 0 +0%
subgraph-idle: style recalc count 10 10 +0% z=-1.4
subgraph-idle: task duration 467ms 514ms +10% ⚠️ z=4.6
subgraph-idle: script duration 12ms 16ms +30% z=-1.6
subgraph-idle: TBT 0ms 0ms +0%
subgraph-idle: heap used 44.5 MB 43.9 MB -1%
subgraph-idle: DOM nodes -281 -129 -54% z=-100.5
subgraph-idle: event listeners -148 -72 -51% variance too high
subgraph-mouse-sweep: avg frame time 17ms 17ms -0% z=-0.1
subgraph-mouse-sweep: p95 frame time 17ms 17ms +0%
subgraph-mouse-sweep: layout duration 4ms 4ms +22% z=-0.8
subgraph-mouse-sweep: style recalc duration 34ms 37ms +11% z=-1.5
subgraph-mouse-sweep: layout count 16 16 +0%
subgraph-mouse-sweep: style recalc count 75 78 +3% z=-1.4
subgraph-mouse-sweep: task duration 763ms 821ms +8% z=0.8
subgraph-mouse-sweep: script duration 85ms 93ms +9% z=-1.2
subgraph-mouse-sweep: TBT 0ms 0ms +0%
subgraph-mouse-sweep: heap used 46.0 MB 44.3 MB -4%
subgraph-mouse-sweep: DOM nodes -278 -126 -55% z=-86.1
subgraph-mouse-sweep: event listeners -148 -148 +0% variance too high
subgraph-transition-enter: avg frame time 17ms 17ms +0%
subgraph-transition-enter: p95 frame time 17ms 17ms +0%
subgraph-transition-enter: layout duration 13ms 12ms -8%
subgraph-transition-enter: style recalc duration 30ms 29ms -4%
subgraph-transition-enter: layout count 14 14 +0%
subgraph-transition-enter: style recalc count 18 18 +0%
subgraph-transition-enter: task duration 925ms 952ms +3%
subgraph-transition-enter: script duration 31ms 36ms +14%
subgraph-transition-enter: TBT 149ms 119ms -20%
subgraph-transition-enter: heap used 94.8 MB 94.9 MB +0%
subgraph-transition-enter: DOM nodes 13673 13673 +0%
subgraph-transition-enter: event listeners 2371 2371 +0%
viewport-pan-sweep: avg frame time 17ms 17ms +0%
viewport-pan-sweep: p95 frame time 17ms 17ms -0%
viewport-pan-sweep: layout duration 0ms 0ms +0%
viewport-pan-sweep: style recalc duration 35ms 41ms +18%
viewport-pan-sweep: layout count 0 0 +0%
viewport-pan-sweep: style recalc count 250 250 -0%
viewport-pan-sweep: task duration 4306ms 4451ms +3%
viewport-pan-sweep: script duration 1334ms 1331ms -0%
viewport-pan-sweep: TBT 0ms 0ms +0%
viewport-pan-sweep: heap used 66.6 MB 66.5 MB -0%
viewport-pan-sweep: DOM nodes -281 -283 +1%
viewport-pan-sweep: event listeners -128 -128 +0%
vue-large-graph-idle: avg frame time 18ms 18ms -0%
vue-large-graph-idle: p95 frame time 17ms 17ms -0%
vue-large-graph-idle: layout duration 0ms 0ms +0%
vue-large-graph-idle: style recalc duration 0ms 0ms +0%
vue-large-graph-idle: layout count 0 0 +0%
vue-large-graph-idle: style recalc count 0 0 +0%
vue-large-graph-idle: task duration 16542ms 17684ms +7%
vue-large-graph-idle: script duration 532ms 587ms +10%
vue-large-graph-idle: TBT 0ms 0ms +0%
vue-large-graph-idle: heap used 159.1 MB 166.3 MB +5%
vue-large-graph-idle: DOM nodes -8312 -8312 +0%
vue-large-graph-idle: event listeners -16387 -16386 -0%
vue-large-graph-pan: avg frame time 18ms 18ms +2%
vue-large-graph-pan: p95 frame time 17ms 17ms +0%
vue-large-graph-pan: layout duration 0ms 0ms +0%
vue-large-graph-pan: style recalc duration 15ms 20ms +36%
vue-large-graph-pan: layout count 0 0 +0%
vue-large-graph-pan: style recalc count 137 146 +6%
vue-large-graph-pan: task duration 20194ms 21382ms +6%
vue-large-graph-pan: script duration 903ms 936ms +4%
vue-large-graph-pan: TBT 102ms 379ms +272%
vue-large-graph-pan: heap used 169.0 MB 158.6 MB -6%
vue-large-graph-pan: DOM nodes -8312 -8312 +0%
vue-large-graph-pan: event listeners -16385 -16383 -0%
workflow-execution: avg frame time 17ms 17ms -0% z=0.1
workflow-execution: p95 frame time 17ms 17ms +0%
workflow-execution: layout duration 1ms 1ms +23% z=-1.6
workflow-execution: style recalc duration 21ms 19ms -7% z=-2.2
workflow-execution: layout count 3 5 +67% z=0.1
workflow-execution: style recalc count 14 12 -14% z=-2.8
workflow-execution: task duration 112ms 109ms -3% z=-1.3
workflow-execution: script duration 9ms 9ms -2% z=-6.8
workflow-execution: TBT 0ms 0ms +0%
workflow-execution: heap used 48.6 MB 57.7 MB +19%
workflow-execution: DOM nodes 121 130 +7% z=-4.3
workflow-execution: event listeners 65 42 -35% z=-2.3
Historical variance (last 15 runs)
Metric μ σ CV
canvas-idle: avg frame time 17ms 0ms 0.0%
canvas-idle: layout duration 0ms 0ms 0.0%
canvas-idle: style recalc duration 11ms 1ms 8.2%
canvas-idle: layout count 0 0 0.0%
canvas-idle: style recalc count 11 1 5.0%
canvas-idle: task duration 395ms 31ms 7.9%
canvas-idle: script duration 25ms 2ms 8.8%
canvas-idle: TBT 0ms 0ms 0.0%
canvas-idle: DOM nodes 23 1 5.6%
canvas-idle: event listeners 12 5 40.9%
canvas-mouse-sweep: avg frame time 17ms 0ms 0.0%
canvas-mouse-sweep: layout duration 4ms 0ms 5.4%
canvas-mouse-sweep: style recalc duration 43ms 3ms 7.4%
canvas-mouse-sweep: layout count 12 0 0.0%
canvas-mouse-sweep: style recalc count 79 2 3.0%
canvas-mouse-sweep: task duration 865ms 58ms 6.7%
canvas-mouse-sweep: script duration 136ms 6ms 4.8%
canvas-mouse-sweep: TBT 0ms 0ms 0.0%
canvas-mouse-sweep: DOM nodes 62 3 4.2%
canvas-mouse-sweep: event listeners 8 4 49.4%
canvas-zoom-sweep: avg frame time 17ms 0ms 0.0%
canvas-zoom-sweep: layout duration 1ms 0ms 7.0%
canvas-zoom-sweep: style recalc duration 19ms 2ms 8.0%
canvas-zoom-sweep: layout count 6 0 0.0%
canvas-zoom-sweep: style recalc count 31 0 1.5%
canvas-zoom-sweep: task duration 327ms 23ms 7.1%
canvas-zoom-sweep: script duration 27ms 3ms 11.1%
canvas-zoom-sweep: TBT 0ms 0ms 0.0%
canvas-zoom-sweep: DOM nodes 79 1 1.0%
canvas-zoom-sweep: event listeners 24 5 21.8%
dom-widget-clipping: avg frame time 17ms 0ms 0.0%
dom-widget-clipping: layout duration 0ms 0ms 0.0%
dom-widget-clipping: style recalc duration 10ms 1ms 8.0%
dom-widget-clipping: layout count 0 0 0.0%
dom-widget-clipping: style recalc count 13 0 3.8%
dom-widget-clipping: task duration 365ms 16ms 4.5%
dom-widget-clipping: script duration 68ms 3ms 4.8%
dom-widget-clipping: TBT 0ms 0ms 0.0%
dom-widget-clipping: DOM nodes 22 1 6.4%
dom-widget-clipping: event listeners 8 6 81.2%
large-graph-idle: avg frame time 17ms 0ms 0.0%
large-graph-idle: layout duration 0ms 0ms 0.0%
large-graph-idle: style recalc duration 12ms 1ms 8.6%
large-graph-idle: layout count 0 0 0.0%
large-graph-idle: style recalc count 12 0 2.7%
large-graph-idle: task duration 542ms 54ms 10.0%
large-graph-idle: script duration 102ms 11ms 10.3%
large-graph-idle: TBT 0ms 0ms 0.0%
large-graph-idle: DOM nodes 25 1 3.7%
large-graph-idle: event listeners 26 6 23.2%
large-graph-pan: avg frame time 17ms 0ms 0.0%
large-graph-pan: layout duration 0ms 0ms 0.0%
large-graph-pan: style recalc duration 17ms 1ms 4.6%
large-graph-pan: layout count 0 0 0.0%
large-graph-pan: style recalc count 70 1 0.9%
large-graph-pan: task duration 1082ms 43ms 4.0%
large-graph-pan: script duration 408ms 20ms 4.8%
large-graph-pan: TBT 0ms 0ms 0.0%
large-graph-pan: DOM nodes 19 2 8.7%
large-graph-pan: event listeners 5 1 16.8%
minimap-idle: avg frame time 17ms 0ms 0.0%
minimap-idle: layout duration 0ms 0ms 0.0%
minimap-idle: style recalc duration 10ms 1ms 8.6%
minimap-idle: layout count 0 0 0.0%
minimap-idle: style recalc count 10 1 7.1%
minimap-idle: task duration 527ms 47ms 9.0%
minimap-idle: script duration 98ms 10ms 10.1%
minimap-idle: TBT 0ms 0ms 0.0%
minimap-idle: DOM nodes 19 1 7.1%
minimap-idle: event listeners 5 1 14.4%
subgraph-dom-widget-clipping: avg frame time 17ms 0ms 0.0%
subgraph-dom-widget-clipping: layout duration 0ms 0ms 0.0%
subgraph-dom-widget-clipping: style recalc duration 13ms 1ms 7.4%
subgraph-dom-widget-clipping: layout count 0 0 0.0%
subgraph-dom-widget-clipping: style recalc count 48 1 1.2%
subgraph-dom-widget-clipping: task duration 378ms 18ms 4.9%
subgraph-dom-widget-clipping: script duration 128ms 6ms 4.9%
subgraph-dom-widget-clipping: TBT 0ms 0ms 0.0%
subgraph-dom-widget-clipping: DOM nodes 22 1 5.0%
subgraph-dom-widget-clipping: event listeners 16 6 36.0%
subgraph-idle: avg frame time 17ms 0ms 0.0%
subgraph-idle: layout duration 0ms 0ms 0.0%
subgraph-idle: style recalc duration 10ms 1ms 7.5%
subgraph-idle: layout count 0 0 0.0%
subgraph-idle: style recalc count 11 1 6.0%
subgraph-idle: task duration 370ms 31ms 8.5%
subgraph-idle: script duration 20ms 3ms 13.2%
subgraph-idle: TBT 0ms 0ms 0.0%
subgraph-idle: DOM nodes 22 1 6.9%
subgraph-idle: event listeners 10 7 64.5%
subgraph-mouse-sweep: avg frame time 17ms 0ms 0.0%
subgraph-mouse-sweep: layout duration 5ms 0ms 6.8%
subgraph-mouse-sweep: style recalc duration 42ms 3ms 7.8%
subgraph-mouse-sweep: layout count 16 0 0.0%
subgraph-mouse-sweep: style recalc count 80 2 2.4%
subgraph-mouse-sweep: task duration 766ms 69ms 9.0%
subgraph-mouse-sweep: script duration 101ms 7ms 6.5%
subgraph-mouse-sweep: TBT 0ms 0ms 0.0%
subgraph-mouse-sweep: DOM nodes 67 2 3.3%
subgraph-mouse-sweep: event listeners 8 4 52.6%
workflow-execution: avg frame time 17ms 0ms 0.0%
workflow-execution: layout duration 2ms 0ms 9.4%
workflow-execution: style recalc duration 24ms 2ms 9.1%
workflow-execution: layout count 5 1 11.0%
workflow-execution: style recalc count 18 2 11.5%
workflow-execution: task duration 123ms 11ms 8.8%
workflow-execution: script duration 29ms 3ms 10.2%
workflow-execution: TBT 0ms 0ms 0.0%
workflow-execution: DOM nodes 161 7 4.4%
workflow-execution: event listeners 52 4 8.4%
Trend (last 15 commits on main)
Metric Trend Dir Latest
canvas-idle: avg frame time ▆▃▆▁▆▃▆█▆▆▄▃▃▄▃ ➡️ 17ms
canvas-idle: p95 frame time ➡️ NaNms
canvas-idle: layout duration ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
canvas-idle: style recalc duration ▇▇▆▆▃█▄▃▄▃▇▄▁▆▇ ➡️ 11ms
canvas-idle: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0
canvas-idle: style recalc count █▃▅▂▅▆▃▁▂▁▂▅▆▅▆ ➡️ 12
canvas-idle: task duration ▃▃▃▆▂▃▃▅▆▂█▃▁▃▃ ➡️ 391ms
canvas-idle: script duration ▄▃▅▇▂▅▃▆▇▅█▄▁▅▆ ➡️ 27ms
canvas-idle: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
canvas-idle: heap used ➡️ NaN MB
canvas-idle: DOM nodes █▇▆▅▃▇▃▁▂▂▅▆▆▆▇ ➡️ 24
canvas-idle: event listeners ▅█▅▄▁▅▁▁▁▄▅▅▁▅▄ 📉 11
canvas-mouse-sweep: avg frame time ▆█▆▃▁▃▁▆▆▁▃▆▆▃▃ ➡️ 17ms
canvas-mouse-sweep: p95 frame time ➡️ NaNms
canvas-mouse-sweep: layout duration ▁▃▂▄▁▂▁▃▆▂█▇▆▄▃ ➡️ 4ms
canvas-mouse-sweep: style recalc duration ▄▄▂▄▁▂▃▃▅▄█▆▂▄▄ ➡️ 43ms
canvas-mouse-sweep: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 12
canvas-mouse-sweep: style recalc count █▅▄▃▂▂▁▄▄▅▆▅▂▇▄ ➡️ 79
canvas-mouse-sweep: task duration █▆▄▂▂▃▂▄▄▅█▆▁▆▄ ➡️ 868ms
canvas-mouse-sweep: script duration ▄▅▄▆▄▆▆▆▅▅█▆▁▅▆ ➡️ 139ms
canvas-mouse-sweep: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
canvas-mouse-sweep: heap used ➡️ NaN MB
canvas-mouse-sweep: DOM nodes █▅▃▃▁▂▂▃▂▄▆▅▃▅▅ ➡️ 64
canvas-mouse-sweep: event listeners █▁▁▁▁▁▇▁▁▁██▇▁█ 📈 13
canvas-zoom-sweep: avg frame time ▅▅█▄▅▁▁▁▅▁▁▅▄▅▁ ➡️ 17ms
canvas-zoom-sweep: p95 frame time ➡️ NaNms
canvas-zoom-sweep: layout duration ▆▅▅▄▁▁█▅▃▅▇▆▁▂▆ ➡️ 1ms
canvas-zoom-sweep: style recalc duration ▆▅▄▆▅▃█▆▇▅▇▄▁▃▅ ➡️ 20ms
canvas-zoom-sweep: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 6
canvas-zoom-sweep: style recalc count ▁▁▃▄▆▃▆█▄▄▆▁▆▁▆ ➡️ 32
canvas-zoom-sweep: task duration ▄▂▁▇▂▂▄▅▆▃█▄▁▁▅ ➡️ 338ms
canvas-zoom-sweep: script duration ▃▃▂▇▂▂▅▇▆▅█▄▁▂▆ ➡️ 30ms
canvas-zoom-sweep: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
canvas-zoom-sweep: heap used ➡️ NaN MB
canvas-zoom-sweep: DOM nodes ▄▃▁▅█▁▃▆▄▅▅▃▃▄▃ ➡️ 79
canvas-zoom-sweep: event listeners ▁▁▂▅█▂▁▅▁▅▅▄▁▅▁ ➡️ 19
dom-widget-clipping: avg frame time ▂▄▅▅▂▄█▇▅▇▇▅▅▁▇ ➡️ 17ms
dom-widget-clipping: p95 frame time ➡️ NaNms
dom-widget-clipping: layout duration ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
dom-widget-clipping: style recalc duration ▆▆▂▆▄▃██▄▁▆▇▆▃▅ ➡️ 10ms
dom-widget-clipping: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0
dom-widget-clipping: style recalc count ▇█▅█▅▄█▇▇▁▇▄▇▂▅ ➡️ 13
dom-widget-clipping: task duration ▃▃▁▅▄▃▅▆▅▂▇█▁▅▅ ➡️ 371ms
dom-widget-clipping: script duration ▅▄▄▆▆▅▇▇▆▃█▇▁▇▇ ➡️ 71ms
dom-widget-clipping: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
dom-widget-clipping: heap used ➡️ NaN MB
dom-widget-clipping: DOM nodes ▇▇▄▇▅▄█▇▅▁▅▄▇▃▄ ➡️ 21
dom-widget-clipping: event listeners ▅▅▅▅▁▅██▁▁▁▁█▁▁ 📉 2
large-graph-idle: avg frame time ▅▅▅▅▅▂▁▂▄▅▄▂▂▅█ ➡️ 17ms
large-graph-idle: p95 frame time ➡️ NaNms
large-graph-idle: layout duration ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
large-graph-idle: style recalc duration ▅▅▅▆▄▅▃▄▅▅▆█▁▄▆ ➡️ 13ms
large-graph-idle: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0
large-graph-idle: style recalc count █▆█▃▃▁▃▆▃▆▆▃▆██ ➡️ 12
large-graph-idle: task duration ▂▃▂▆▂▃▃▇▅▃██▁▂▅ ➡️ 569ms
large-graph-idle: script duration ▄▅▄▆▄▅▅▇▆▅█▆▁▃▆ ➡️ 110ms
large-graph-idle: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
large-graph-idle: heap used ➡️ NaN MB
large-graph-idle: DOM nodes ▆█▅▂▅▃▁▂▃▅▅▆▂▆▅ ➡️ 25
large-graph-idle: event listeners ███▇██▄▁▄▇▇█▂█▇ ➡️ 29
large-graph-pan: avg frame time ▆▃▃▆█▃▁█▆▆▆▆█▁▆ ➡️ 17ms
large-graph-pan: p95 frame time ➡️ NaNms
large-graph-pan: layout duration ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
large-graph-pan: style recalc duration ▃▂▄▄▁▅▂▂▁▄▄█▃▁▂ ➡️ 17ms
large-graph-pan: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0
large-graph-pan: style recalc count ▆▃█▂▃▂▂▂▁▇▅▃█▆▃ ➡️ 69
large-graph-pan: task duration ▄▃▄▆▄▄▄▆▄▄█▆▁▂▅ ➡️ 1100ms
large-graph-pan: script duration ▅▄▅▆▆▅▄▆▄▅█▄▁▄▅ ➡️ 413ms
large-graph-pan: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
large-graph-pan: heap used ➡️ NaN MB
large-graph-pan: DOM nodes ▅▃▆▂▄▁▃▁▁▅▁▂█▅▂ ➡️ 18
large-graph-pan: event listeners █▆█▁▁▆▁▁▃▆▁▃██▃ ➡️ 5
minimap-idle: avg frame time ▃▆▆▃█▁█▆▆▃▃▆█▆█ ➡️ 17ms
minimap-idle: p95 frame time ➡️ NaNms
minimap-idle: layout duration ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
minimap-idle: style recalc duration ▄█▁█▅▅█▅▅▃▅▁▁▄▆ ➡️ 10ms
minimap-idle: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0
minimap-idle: style recalc count ▃▅▂▄█▃▆▁▂▅▂▁▅▆▃ ➡️ 9
minimap-idle: task duration ▃▄▁▅▁▃▄▅▇▃█▅▁▁▅ ➡️ 547ms
minimap-idle: script duration ▄▆▃▇▃▅▆▆▇▅█▅▁▃▆ ➡️ 106ms
minimap-idle: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
minimap-idle: heap used ➡️ NaN MB
minimap-idle: DOM nodes ▃▅▂▄█▃▆▁▂▅▂▁▅▆▃ ➡️ 19
minimap-idle: event listeners ▃▃▆▁▁▁▃▁▁▆▁▃█▆▁ ➡️ 4
subgraph-dom-widget-clipping: avg frame time ▅▄▄▄▄▄█▄▄▄▃▁▆▃▃ ➡️ 17ms
subgraph-dom-widget-clipping: p95 frame time ➡️ NaNms
subgraph-dom-widget-clipping: layout duration ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
subgraph-dom-widget-clipping: style recalc duration ▂▄▃▅▅▃▂▅▇▃▄█▁▄▆ ➡️ 14ms
subgraph-dom-widget-clipping: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0
subgraph-dom-widget-clipping: style recalc count ▇█▆▃▆▃▁▆█▇▃▆▇█▅ ➡️ 48
subgraph-dom-widget-clipping: task duration ▂▃▃▆▅▅▂▅█▂▆█▁▂▇ ➡️ 398ms
subgraph-dom-widget-clipping: script duration ▃▃▃▄▅▅▂▄█▂▅▇▁▂▅ ➡️ 131ms
subgraph-dom-widget-clipping: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
subgraph-dom-widget-clipping: heap used ➡️ NaN MB
subgraph-dom-widget-clipping: DOM nodes ▅▇▅▂▅▂▁▅▅▅▁▇▅█▄ ➡️ 22
subgraph-dom-widget-clipping: event listeners ▅▅▅▂▅▁▅██▁▁█▅█▅ 📈 16
subgraph-idle: avg frame time ▆▆█▁▆▃▆▆▆▃▆▁▃▆█ ➡️ 17ms
subgraph-idle: p95 frame time ➡️ NaNms
subgraph-idle: layout duration ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
subgraph-idle: style recalc duration ▁▇▃▆▂▄▂▃▃▆▆▄▃▇█ ➡️ 12ms
subgraph-idle: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0
subgraph-idle: style recalc count ▃▆▃▃▂▅▁▂▁▆▃▃██▇ ➡️ 12
subgraph-idle: task duration ▁▃▁▇▁▁▃▆▅▂█▅▁▁▄ ➡️ 378ms
subgraph-idle: script duration ▁▃▂▇▁▂▃▇▆▂█▅▂▁▅ ➡️ 22ms
subgraph-idle: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
subgraph-idle: heap used ➡️ NaN MB
subgraph-idle: DOM nodes ▃▅▃▂▁▄▁▂▁▅▃▂▇█▇ ➡️ 24
subgraph-idle: event listeners ▁▅▁▁▁▁▁▁▁▅▄▁███ 📈 21
subgraph-mouse-sweep: avg frame time ▅▄▁▃▃▄▆▄▆▃▃█▁▃▃ ➡️ 17ms
subgraph-mouse-sweep: p95 frame time ➡️ NaNms
subgraph-mouse-sweep: layout duration ▁▄▄▄▃▃▅▅▅▂█▇▂▃▆ ➡️ 5ms
subgraph-mouse-sweep: style recalc duration ▃▂▄▅▂▃▄▅█▃█▆▁▂▅ ➡️ 43ms
subgraph-mouse-sweep: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 16
subgraph-mouse-sweep: style recalc count ▅▂▅▅▁▄▃▅█▅▆▄▂▄▅ ➡️ 81
subgraph-mouse-sweep: task duration ▃▂▄▅▂▄▄▅▇▄█▆▁▃▅ ➡️ 785ms
subgraph-mouse-sweep: script duration ▄▅▄▇▅▅▆▇▆▅██▁▄▆ ➡️ 105ms
subgraph-mouse-sweep: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
subgraph-mouse-sweep: heap used ➡️ NaN MB
subgraph-mouse-sweep: DOM nodes ▅▁▄▅▁▄▃▃█▅▅▄▂▅▃ ➡️ 66
subgraph-mouse-sweep: event listeners ▇▁▂▇▁▂▂▂█▇▂▂▇▇▂ 📈 5
workflow-execution: avg frame time ▆▆▆▄▆▆▃▄▁▄█▆▅▄▆ ➡️ 17ms
workflow-execution: p95 frame time ➡️ NaNms
workflow-execution: layout duration ▁▆▁▃▂▄▃▂▃▃▅█▄▂▅ ➡️ 2ms
workflow-execution: style recalc duration ▃▇▅▇▁▅▆▇█▁██▂▄▆ ➡️ 25ms
workflow-execution: layout count ▁█▂▃▂▃▃▁▃▃▄▃▂▃▂ ➡️ 5
workflow-execution: style recalc count ▃█▅▇▁▄▅▆▅▅▅▅▄▄▂ ➡️ 15
workflow-execution: task duration ▂▅▄▅▁▄▆▆▆▁▇█▁▃▃ ➡️ 120ms
workflow-execution: script duration ▄▃▄▄▃▅▄▅▆▂▇█▁▃▄ ➡️ 29ms
workflow-execution: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
workflow-execution: heap used ➡️ NaN MB
workflow-execution: DOM nodes ▂█▃▆▁▄▃▅▃█▃▃▄▃▁ ➡️ 152
workflow-execution: event listeners ▅███▁▅███▁██▅█▅ ➡️ 49
Raw data
{
  "timestamp": "2026-07-31T08:17:46.400Z",
  "gitSha": "e05186bfaa4ffa2193004e3d4853bc728c8335f7",
  "branch": "christian/frontend-state-guidance",
  "measurements": [
    {
      "name": "canvas-idle",
      "durationMs": 2135.6229999999814,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 7.315,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 565.778,
      "heapDeltaBytes": 4902360,
      "heapUsedBytes": 72903956,
      "domNodes": 18,
      "jsHeapTotalBytes": 20049920,
      "scriptDurationMs": 22.229999999999997,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "canvas-idle",
      "durationMs": 2020.762999999988,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 8.070999999999998,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 470.105,
      "heapDeltaBytes": 4453148,
      "heapUsedBytes": 72230644,
      "domNodes": 18,
      "jsHeapTotalBytes": 20574208,
      "scriptDurationMs": 17.071,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 1975.2379999999903,
      "styleRecalcs": 79,
      "styleRecalcDurationMs": 52.21600000000001,
      "layouts": 12,
      "layoutDurationMs": 4.244,
      "taskDurationMs": 982.5010000000001,
      "heapDeltaBytes": -16039388,
      "heapUsedBytes": 51699096,
      "domNodes": -279,
      "jsHeapTotalBytes": 21225472,
      "scriptDurationMs": 127.02100000000003,
      "eventListeners": -146,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 1863.7339999999654,
      "styleRecalcs": 75,
      "styleRecalcDurationMs": 37.851000000000006,
      "layouts": 12,
      "layoutDurationMs": 3.758,
      "taskDurationMs": 901.5620000000001,
      "heapDeltaBytes": -15434800,
      "heapUsedBytes": 52385172,
      "domNodes": -280,
      "jsHeapTotalBytes": 20701184,
      "scriptDurationMs": 119.00800000000001,
      "eventListeners": -148,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1741.199999999992,
      "styleRecalcs": 31,
      "styleRecalcDurationMs": 17.096,
      "layouts": 6,
      "layoutDurationMs": 0.6200000000000001,
      "taskDurationMs": 398.21799999999996,
      "heapDeltaBytes": 7889860,
      "heapUsedBytes": 75726816,
      "domNodes": 76,
      "jsHeapTotalBytes": 20312064,
      "scriptDurationMs": 21.624000000000002,
      "eventListeners": 19,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1771.729999999934,
      "styleRecalcs": 32,
      "styleRecalcDurationMs": 17.019,
      "layouts": 6,
      "layoutDurationMs": 0.6119999999999999,
      "taskDurationMs": 392.752,
      "heapDeltaBytes": 7913584,
      "heapUsedBytes": 75794568,
      "domNodes": 77,
      "jsHeapTotalBytes": 20049920,
      "scriptDurationMs": 20.627999999999997,
      "eventListeners": 19,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 611.6309999999885,
      "styleRecalcs": 11,
      "styleRecalcDurationMs": 7.533000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 394.652,
      "heapDeltaBytes": -12483356,
      "heapUsedBytes": 55413000,
      "domNodes": 18,
      "jsHeapTotalBytes": 22147072,
      "scriptDurationMs": 58.373,
      "eventListeners": 0,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 579.0600000000268,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 5.428000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 377.01300000000003,
      "heapDeltaBytes": -11958708,
      "heapUsedBytes": 55882836,
      "domNodes": 14,
      "jsHeapTotalBytes": 21622784,
      "scriptDurationMs": 58.213,
      "eventListeners": 0,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2028.299000000004,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 7.122,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 682.879,
      "heapDeltaBytes": 8373628,
      "heapUsedBytes": 67385500,
      "domNodes": -284,
      "jsHeapTotalBytes": 4780032,
      "scriptDurationMs": 100.57600000000001,
      "eventListeners": -144,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2026.267999999959,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 7.130000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 678.189,
      "heapDeltaBytes": 7976384,
      "heapUsedBytes": 67377820,
      "domNodes": -283,
      "jsHeapTotalBytes": 5304320,
      "scriptDurationMs": 99.02,
      "eventListeners": -142,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-pan",
      "durationMs": 2171.5090000000146,
      "styleRecalcs": 68,
      "styleRecalcDurationMs": 14.569000000000003,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1256.7979999999998,
      "heapDeltaBytes": 6179576,
      "heapUsedBytes": 65890008,
      "domNodes": -285,
      "jsHeapTotalBytes": 5005312,
      "scriptDurationMs": 408.754,
      "eventListeners": -142,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-pan",
      "durationMs": 2139.2769999999928,
      "styleRecalcs": 68,
      "styleRecalcDurationMs": 14.196,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1307.328,
      "heapDeltaBytes": 2730632,
      "heapUsedBytes": 62868328,
      "domNodes": -285,
      "jsHeapTotalBytes": 5529600,
      "scriptDurationMs": 444.03800000000007,
      "eventListeners": -144,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 3164.8999999999887,
      "styleRecalcs": 64,
      "styleRecalcDurationMs": 14.915999999999995,
      "layouts": 60,
      "layoutDurationMs": 8.131,
      "taskDurationMs": 1488.1899999999998,
      "heapDeltaBytes": 24052456,
      "heapUsedBytes": 86073552,
      "domNodes": 10,
      "jsHeapTotalBytes": 7864320,
      "scriptDurationMs": 527.3149999999999,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 3212.248000000045,
      "styleRecalcs": 65,
      "styleRecalcDurationMs": 16.312000000000005,
      "layouts": 60,
      "layoutDurationMs": 8.126,
      "taskDurationMs": 1527.8990000000001,
      "heapDeltaBytes": -277288,
      "heapUsedBytes": 61162108,
      "domNodes": -287,
      "jsHeapTotalBytes": 8187904,
      "scriptDurationMs": 530.277,
      "eventListeners": -148,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "minimap-idle",
      "durationMs": 2027.0029999999792,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 7.158999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 687.663,
      "heapDeltaBytes": 7788264,
      "heapUsedBytes": 68552668,
      "domNodes": -282,
      "jsHeapTotalBytes": 5042176,
      "scriptDurationMs": 102.17899999999999,
      "eventListeners": -144,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "minimap-idle",
      "durationMs": 2028.6619999999402,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 6.831,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 691.335,
      "heapDeltaBytes": 7908268,
      "heapUsedBytes": 68692208,
      "domNodes": -283,
      "jsHeapTotalBytes": 4780032,
      "scriptDurationMs": 99.45199999999998,
      "eventListeners": -144,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 613.7819999999579,
      "styleRecalcs": 47,
      "styleRecalcDurationMs": 11.305000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 412.42,
      "heapDeltaBytes": -10459764,
      "heapUsedBytes": 57452536,
      "domNodes": 20,
      "jsHeapTotalBytes": 20836352,
      "scriptDurationMs": 121.314,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 624.1049999999859,
      "styleRecalcs": 47,
      "styleRecalcDurationMs": 11.461999999999998,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 429.38100000000003,
      "heapDeltaBytes": -12056648,
      "heapUsedBytes": 55743936,
      "domNodes": 20,
      "jsHeapTotalBytes": 22409216,
      "scriptDurationMs": 124.17299999999999,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "subgraph-idle",
      "durationMs": 2021.269000000018,
      "styleRecalcs": 11,
      "styleRecalcDurationMs": 9.306,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 509.065,
      "heapDeltaBytes": -22117680,
      "heapUsedBytes": 45732420,
      "domNodes": 22,
      "jsHeapTotalBytes": 20701184,
      "scriptDurationMs": 14.878000000000002,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-idle",
      "durationMs": 2039.4890000000032,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 7.568000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 518.965,
      "heapDeltaBytes": -21859016,
      "heapUsedBytes": 46374884,
      "domNodes": -279,
      "jsHeapTotalBytes": 20963328,
      "scriptDurationMs": 16.831000000000007,
      "eventListeners": -148,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1737.2589999999946,
      "styleRecalcs": 76,
      "styleRecalcDurationMs": 36.027,
      "layouts": 16,
      "layoutDurationMs": 4.283,
      "taskDurationMs": 821.5409999999999,
      "heapDeltaBytes": -21055028,
      "heapUsedBytes": 46912016,
      "domNodes": -281,
      "jsHeapTotalBytes": 20701184,
      "scriptDurationMs": 91.483,
      "eventListeners": -148,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1728.4270000000106,
      "styleRecalcs": 79,
      "styleRecalcDurationMs": 38.694,
      "layouts": 16,
      "layoutDurationMs": 4.63,
      "taskDurationMs": 820.1700000000001,
      "heapDeltaBytes": -21867040,
      "heapUsedBytes": 46029916,
      "domNodes": 30,
      "jsHeapTotalBytes": 20439040,
      "scriptDurationMs": 94.51799999999999,
      "eventListeners": -148,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-transition-enter",
      "durationMs": 1384.9119999999857,
      "styleRecalcs": 18,
      "styleRecalcDurationMs": 28.899000000000008,
      "layouts": 14,
      "layoutDurationMs": 11.619000000000002,
      "taskDurationMs": 951.5930000000002,
      "heapDeltaBytes": 31093496,
      "heapUsedBytes": 99558328,
      "domNodes": 13673,
      "jsHeapTotalBytes": 14942208,
      "scriptDurationMs": 35.891999999999996,
      "eventListeners": 2371,
      "totalBlockingTimeMs": 119,
      "frameDurationMs": 16.670000000000012,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 8185.944000000007,
      "styleRecalcs": 250,
      "styleRecalcDurationMs": 42.403,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 4475.282,
      "heapDeltaBytes": 10739248,
      "heapUsedBytes": 69858364,
      "domNodes": -283,
      "jsHeapTotalBytes": 7069696,
      "scriptDurationMs": 1332.104,
      "eventListeners": -128,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333338,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 8253.021999999986,
      "styleRecalcs": 249,
      "styleRecalcDurationMs": 40.248,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 4426.8640000000005,
      "heapDeltaBytes": 10498972,
      "heapUsedBytes": 69498100,
      "domNodes": -282,
      "jsHeapTotalBytes": 7593984,
      "scriptDurationMs": 1329.2350000000001,
      "eventListeners": -128,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 17805.060999999965,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 17750.596,
      "heapDeltaBytes": -54763820,
      "heapUsedBytes": 167374732,
      "domNodes": -8312,
      "jsHeapTotalBytes": -13312000,
      "scriptDurationMs": 608.7589999999999,
      "eventListeners": -16387,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.776666666666763,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 17640.61700000002,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 17618.163,
      "heapDeltaBytes": -34088528,
      "heapUsedBytes": 181319692,
      "domNodes": -8312,
      "jsHeapTotalBytes": -14360576,
      "scriptDurationMs": 565.939,
      "eventListeners": -16385,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.77333333333336,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 21503.59500000002,
      "styleRecalcs": 148,
      "styleRecalcDurationMs": 21.08199999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 21467.649999999998,
      "heapDeltaBytes": -80334456,
      "heapUsedBytes": 150583320,
      "domNodes": -8312,
      "jsHeapTotalBytes": -15671296,
      "scriptDurationMs": 969.9579999999999,
      "eventListeners": -16381,
      "totalBlockingTimeMs": 385,
      "frameDurationMs": 18.333333333333332,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 21340.538000000037,
      "styleRecalcs": 143,
      "styleRecalcDurationMs": 19.65800000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 21295.602,
      "heapDeltaBytes": -25235628,
      "heapUsedBytes": 181995428,
      "domNodes": -8312,
      "jsHeapTotalBytes": -11542528,
      "scriptDurationMs": 903.029,
      "eventListeners": -16385,
      "totalBlockingTimeMs": 373,
      "frameDurationMs": 17.780000000000047,
      "p95FrameDurationMs": 16.80000000000291
    },
    {
      "name": "workflow-execution",
      "durationMs": 473.1830000000059,
      "styleRecalcs": 14,
      "styleRecalcDurationMs": 19.910999999999998,
      "layouts": 3,
      "layoutDurationMs": 0.7010000000000001,
      "taskDurationMs": 120.39199999999998,
      "heapDeltaBytes": -15981688,
      "heapUsedBytes": 51027204,
      "domNodes": 132,
      "jsHeapTotalBytes": 7729152,
      "scriptDurationMs": 12.068000000000001,
      "eventListeners": 67,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "workflow-execution",
      "durationMs": 154.11100000005717,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 18.831,
      "layouts": 7,
      "layoutDurationMs": 1.9409999999999998,
      "taskDurationMs": 97.333,
      "heapDeltaBytes": 2850004,
      "heapUsedBytes": 69930328,
      "domNodes": 128,
      "jsHeapTotalBytes": 2883584,
      "scriptDurationMs": 6.021999999999998,
      "eventListeners": 17,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66666666666665,
      "p95FrameDurationMs": 16.700000000000728
    }
  ]
}

@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

@@           Coverage Diff           @@
##             main   #14472   +/-   ##
=======================================
  Coverage   78.24%   78.24%           
=======================================
  Files        1767     1767           
  Lines      110746   110746           
  Branches    35063    35063           
=======================================
  Hits        86655    86655           
  Misses      23539    23539           
  Partials      552      552           
Flag Coverage Δ
unit 70.22% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@christian-byrne

Copy link
Copy Markdown
Contributor Author

Ran the rule against current main (rebased this branch onto it) to get the
authoritative violation list, so whoever promotes it to error does not have to
rediscover any of this. Four warnings, three files — and they are not the same
kind of problem.

src/components/maskeditor/BrushCursor.vue         60:22, 71:22  (measure)
src/components/breadcrumb/SubgraphBreadcrumb.vue  142:16        (inspect)
src/components/topbar/WorkflowTabs.vue            218:6         (inspect)

One is a real defect, not a lint nit

BrushCursor.vue:60 and :71. brushLeft and brushTop each call
containerRef?.getBoundingClientRect(), and both depend on store.cursorPoint
— which changes on every mousemove. So painting in the mask editor forces
two synchronous layout reads per pointer event, to fetch a dialog rect that
only changes on resize or reposition.

This is exactly the cost the rule exists to prevent, and it is the one I would
fix first regardless of the rule. The fix is to cache the rect rather than
re-measure it: read it when the dialog opens and on resize, into a
shallowRef, and subtract from that.

I have not written that fix. It needs the dialog's move/resize paths
verified interactively, which I cannot do from here, and a wrong cache
invalidation would misplace the brush cursor — a worse bug than the one being
fixed. Flagging rather than guessing.

Two are benign and only technically in scope

SubgraphBreadcrumb.vue:142 (breadcrumbElement) and
WorkflowTabs.vue:218 (scrollContent) are querySelector lookups, not
layout reads. Neither is on a hot path:

  • breadcrumbElement recomputes only when breadcrumbRef changes, and exists
    to feed watch(breadcrumbElement, …) at line 148. Converting it to a plain
    function would break that watch.
  • scrollContent is read inside scroll() (line 224) and exposed at line
    263
    , so it is part of this component's public surface — changing its shape
    is a small API change, not a local edit.

The honest description of these two is "a computed used as a lazy DOM
accessor," which is a different smell from "a derivation that reads layout."

What that implies for the rule

The current message — "every recompute becomes a layout read" — is accurate
for the getBoundingClientRect selector and not accurate for the
querySelector selector, which is why these two read as false-ish positives.
Worth either splitting the message per selector, or narrowing the
querySelector arm to derivations that are actually consumed reactively.

Either way, promoting to error needs the BrushCursor fix first — the other
two can be silenced accurately with a scoped disable and a comment, since
neither is doing the thing the rule warns about.

Comment thread eslint.config.ts
Comment on lines +266 to +268
// 'warn' rather than 'error' because four pre-existing instances remain, in
// BrushCursor.vue, WorkflowTabs.vue and SubgraphBreadcrumb.vue. Promote to
// 'error' once those are derived from stores instead.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make it error now and fix them here, or in advance of this change?

@DrJKL DrJKL left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love this.
I would like to always introduce linters as Error instead of Warn.
I'll try to remember to update the layoutStore references after the ECS changees get in.

@DrJKL DrJKL assigned christian-byrne and unassigned DrJKL Aug 18, 2026
@christian-byrne
christian-byrne added this pull request to the merge queue Aug 18, 2026
@christian-byrne

Copy link
Copy Markdown
Contributor Author

Thanks @DrJKL — enqueued.

On warn vs error: I agree with the principle and it is the intended end state; warn here is a sequencing constraint rather than a preference. The rule has four pre-existing violations on main (BrushCursor.vue ×2, WorkflowTabs.vue, SubgraphBreadcrumb.vue), so error would fail lint on main the moment this lands. The comment above the rule says so and names them, so the promotion is a mechanical follow-up rather than a thing to rediscover.

One correction to my own note there, worth having on the record: I listed BrushCursor.vue as one of the four to clear, and I have a perf PR out for it — #15384 — but that PR does not clear this rule. It collapses two getBoundingClientRect() calls into one; the read is still inside a computed.

That was deliberate, and it is a genuine tension with the rule I am proposing here. A DOMRect is not reactive, so re-reading it as the cursor moves is exactly what keeps the brush offset correct while the mask editor dialog is dragged or resized. Deriving it from a store instead — the thing this rule tells you to do — needs a ResizeObserver plus a drag hook feeding that store. That is the real fix, and it is larger than a perf tweak.

So the promotion to error is blocked on three files, not two, and BrushCursor.vue is the one that needs actual design rather than a mechanical rewrite. Happy to take that as a follow-up if you want it before the promotion — say the word and I will open it rather than let the warn sit indefinitely, which is how these become permanent.

Noted on the layoutStore references after the ECS changes; I will not touch those here.

Merged via the queue into main with commit e5da708 Aug 18, 2026
73 checks passed
@christian-byrne
christian-byrne deleted the christian/frontend-state-guidance branch August 18, 2026 20:56
comfydesigner pushed a commit that referenced this pull request Aug 20, 2026
…le part (#14472)

Writes down the state-modelling conventions we keep re-explaining in
review, and
adds one lint rule for the part that is mechanically checkable.

### Why

Reviews of anything with a beginning and an end — a wizard, an upload,
an
onboarding flow — keep converging on the same handful of points: several
independent booleans that must agree, values stored that could be
derived, and
effects used to keep two pieces of internal state in sync rather than to
talk to
something outside the app. Each time it gets re-argued from scratch on
the PR,
which is slow for the reviewer and unpleasant for the author, who
reasonably
asks why nobody said so earlier.

`docs/guidance/state-and-effects.md` is glob-loaded on `src/**/*.ts` and
`src/**/*.vue`, so it applies automatically rather than needing to be
found.

### What it says

One state value as a discriminated union instead of N booleans, named
events
rather than assignment scattered across call sites, a pure transition
function so
one place owns the rules, `computed` for anything derivable, and effects
reserved
for synchronising outward. Plus the Vue mapping — state to Pinia,
transition to a
pure function, derivation to `computed`, synchronisation to `watch`.

It deliberately does **not** recommend a state-machine library. There is
no
XState or `createMachine` precedent anywhere in `src/`, and a
hand-rolled union
with a pure `reduce` gets the same guarantees without the dependency.

### The lint rule

`no-restricted-syntax` flagging `getBoundingClientRect`,
`getComputedStyle` and
`querySelector*` inside a `computed`. A derivation that measures the DOM
runs a
layout read on every recompute and cannot be unit-tested without a
browser.

It ships at **`warn`, not `error`**, because there are exactly four
pre-existing
instances and this PR does not fix them:

| File | Count |
| ---- | ----- |
| `src/components/maskeditor/BrushCursor.vue` | 2 |
| `src/components/breadcrumb/SubgraphBreadcrumb.vue` | 1 |
| `src/components/topbar/WorkflowTabs.vue` | 1 |

Promote to `error` once those derive from stores instead. I verified the
count
against `main` at the current head rather than trusting the number I
first wrote.

### Scope

Docs and lint config only — no runtime code, no behaviour change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants