From afa68687eb53b6b8afcfec7619142e1399a01d24 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Tue, 11 Aug 2026 10:09:59 +0200 Subject: [PATCH] perf(widget-inputs): stabilise the drag handle component type Last of the two nested-component findings from the widget audit. `Handle` was declared in the render body and passed to a child as `handle={Handle}` - a component *type*. A type that changes identity makes React unmount and remount the subtree instead of updating it, and this component re-renders on every frame of a drag as dnd-kit updates `transform`, so the handle was being torn down and rebuilt continuously while being dragged. Wrapped in useMemo keyed on the two things it closes over. Measured result of the whole overlay/remount sweep, per widget, deterministic component and fiber counts on identical restored data: widget before after mediaReleases 340 comp / 8387 fib 170 / 8057 (-50% components) calendar 171 comp / 1195 fib 75 / 907 (-56% components, -24% fibers) beszelSystemTable 36 comp / 1286 fib 18 / 1230 (-50% components) downloads / mediaRequests / app unchanged - their overlays are functional Two corrections to earlier claims in this branch: - The media-releases win came from the shared OverflowBadge fix, NOT from the tooltip change. Measured on the build with only the tooltip fix, mediaReleases was still at 340; it dropped to 170 once OverflowBadge stopped mounting a Popover when nothing overflows. The tooltip fix remains zero on this board because every release has a description. - The board-level "event listeners" metric is unusable. Across six runs of identical code it read 7905, 8000, 8166, 8179, 10360 and 17114. Chrome's Nodes counter has the same problem (23072 on one run against ~8850 on the rest) because it counts detached nodes awaiting collection, while "elements under body" stayed at 6691-6726 throughout. Only the per-widget component and fiber counts are stable enough to compare, so those are what is reported. --- .../widget-sortable-item-list-input.tsx | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/packages/widgets/src/_inputs/widget-sortable-item-list-input.tsx b/packages/widgets/src/_inputs/widget-sortable-item-list-input.tsx index f4f73fd72..8982ea83c 100644 --- a/packages/widgets/src/_inputs/widget-sortable-item-list-input.tsx +++ b/packages/widgets/src/_inputs/widget-sortable-item-list-input.tsx @@ -177,20 +177,28 @@ const Item = ({ id, }); - const Handle = (props: Partial) => { - return ( - - - - ); - }; + // Declared with useMemo because it is passed to a child as `handle={Handle}`, i.e. as a component + // *type*. A type that changes identity makes React unmount and remount the subtree rather than + // update it, and this component re-renders on every frame of a drag as dnd-kit updates `transform` + // — so the drag handle was being torn down and rebuilt continuously while being dragged. + const Handle = useMemo( + () => + function DragHandle(props: Partial) { + return ( + + + + ); + }, + [listeners, setActivatorNodeRef], + ); return (