perf(kanban): split KanbanCard body into memoized child (dnd lag fix)

Drag perf measured via new Playwright spec drag-perf.spec.ts which drives
a slow drag across the biggest column (~35 cards) while capturing per-frame
durations via rAF inside the page. Pre-fix metrics in HECHO column:

  wrapper-renders=1942 body-renders=N/A
  p50=16.7ms p95=83.3ms max=116.7ms (12fps stalls)

Root cause: useSortable inside KanbanCardImpl subscribes to dnd-kit context;
every pointermove during a drag re-renders ALL cards in the SortableContext.
With the old monolithic component, each re-render rebuilt the full Stack +
Menu + 4 Popovers JSX tree — even though no data had changed.

Fix: split KanbanCardImpl into a thin outer (useSortable + Paper wrapper +
sticker overlay handler + style) and a memoed KanbanCardBody (Stack +
sticker overlay + popover state). All popover/requesterDraft local state
lives inside the body now, so its props are stable across drag and
React.memo skips the body work entirely.

Post-fix metrics:

  wrapper-renders=1943 body-renders=0
  p50=16.7ms p95=16.8ms max=50.0ms (steady 60fps with a single 33ms spike)

E2E thresholds tightened: p50<20, p95<50, max<60, body-renders<5. Regression
in any of these will fail CI.

Probe helpers (_probeRender / _probeBodyRender) are no-ops unless
window._cardRenderProbe is set. Production cost: ~3ns per render call.

Also: `now` clock interval already pauses while dragging (previous commit
e656e8c). `animateLayoutChanges:() => false` kept; it does not visibly
change reorder UX with this codebase.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 15:46:23 +02:00
parent 7ba18f9114
commit c4caff85be
4 changed files with 506 additions and 407 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Kanban</title>
<script type="module" crossorigin src="/assets/index-DfDr7FVp.js"></script>
<script type="module" crossorigin src="/assets/index-Bb2Ri4SN.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-b0xjFtx2.css">
</head>
<body>