From 3f1f6f8e58dee018fdb80b20c1962dc66908559a Mon Sep 17 00:00:00 2001 From: Aether_254 <67494671+Aether-254@users.noreply.github.com> Date: Mon, 21 Sep 2026 13:53:01 +0100 Subject: [PATCH 1/3] perf(frontend): reduce main-thread work while dock dialogs are open - Dialog: use a proper controlled/uncontrolled open state instead of mirroring the open prop into local state through an effect, and memoize the context value so consumers stop re-rendering on every parent render. - DialogOverlay: promote the backdrop-blur overlay to its own compositor layer so hover-driven dock animations no longer re-blur the whole page. - TabsList: observe only data-state changes on direct children instead of every attribute mutation in the subtree, and schedule the initial read in an animation frame. - TabsContents / MarkdownEditor: coalesce resize-driven state updates into a single animation frame and cancel it on unmount; skip state updates when the line mapping is unchanged. - MotionHighlightItem: skip bounds measurement for inactive items. --- .../animate-ui/effects/motion-highlight.tsx | 18 ++++---- .../components/animate-ui/radix/dialog.tsx | 28 ++++++------ frontend/components/animate-ui/radix/tabs.tsx | 37 +++++++++++----- .../components/common/markdown/Editor.tsx | 43 ++++++++++++------- 4 files changed, 78 insertions(+), 48 deletions(-) diff --git a/frontend/components/animate-ui/effects/motion-highlight.tsx b/frontend/components/animate-ui/effects/motion-highlight.tsx index 0c8d719f..a0b9a646 100644 --- a/frontend/components/animate-ui/effects/motion-highlight.tsx +++ b/frontend/components/animate-ui/effects/motion-highlight.tsx @@ -368,7 +368,6 @@ function MotionHighlightItem({ setActiveValue, mode, setBounds, - clearBounds, hover, enabled, className: contextClassName, @@ -392,7 +391,8 @@ function MotionHighlightItem({ React.useEffect(() => { if (mode !== 'parent') return; - let rafId: number; + if (!isActive) return; + let rafId: number | null = null; let previousBounds: Bounds | null = null; const shouldUpdateBounds = forceUpdateBounds === true || @@ -421,18 +421,18 @@ function MotionHighlightItem({ setBounds(bounds); }; - if (isActive) { - updateBounds(); - setActiveClassName(activeClassName ?? ''); - } else if (!activeValue) clearBounds(); + updateBounds(); + setActiveClassName(activeClassName ?? ''); - if (shouldUpdateBounds) return () => cancelAnimationFrame(rafId); + if (shouldUpdateBounds) { + return () => { + if (rafId !== null) cancelAnimationFrame(rafId); + }; + } }, [ mode, isActive, - activeValue, setBounds, - clearBounds, activeClassName, setActiveClassName, forceUpdateBounds, diff --git a/frontend/components/animate-ui/radix/dialog.tsx b/frontend/components/animate-ui/radix/dialog.tsx index 16aaea7a..8c14b7e3 100644 --- a/frontend/components/animate-ui/radix/dialog.tsx +++ b/frontend/components/animate-ui/radix/dialog.tsx @@ -31,28 +31,30 @@ const useDialog = (): DialogContextType => { type DialogProps = React.ComponentProps; -function Dialog({children, ...props}: DialogProps) { - const [isOpen, setIsOpen] = React.useState( - props?.open ?? props?.defaultOpen ?? false, +function Dialog({children, open, defaultOpen, onOpenChange, ...props}: DialogProps) { + const [uncontrolledOpen, setUncontrolledOpen] = React.useState( + defaultOpen ?? false, ); - - React.useEffect(() => { - if (props?.open !== undefined) setIsOpen(props.open); - }, [props?.open]); + const isControlled = open !== undefined; + const isOpen = open ?? uncontrolledOpen; const handleOpenChange = React.useCallback( - (open: boolean) => { - setIsOpen(open); - props.onOpenChange?.(open); + (nextOpen: boolean) => { + if (!isControlled) setUncontrolledOpen(nextOpen); + onOpenChange?.(nextOpen); }, - [props], + [isControlled, onOpenChange], ); + const contextValue = React.useMemo(() => ({isOpen}), [isOpen]); + return ( - + {children} @@ -86,7 +88,7 @@ function DialogOverlay({className, ...props}: DialogOverlayProps) { { - getActiveValue(); + const container = localRef.current; + if (!container) return; + const frameId = requestAnimationFrame(getActiveValue); - const observer = new MutationObserver(getActiveValue); + const observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + if ( + mutation.type === 'attributes' && + mutation.attributeName === 'data-value' + ) { + continue; + } + getActiveValue(); + return; + } + }); - if (localRef.current) { - observer.observe(localRef.current, { - attributes: true, - childList: true, - subtree: true, - }); - } + observer.observe(container, { + attributes: true, + attributeFilter: ['data-state'], + childList: true, + subtree: false, + }); return () => { + cancelAnimationFrame(frameId); observer.disconnect(); }; }, [getActiveValue]); @@ -193,10 +206,13 @@ function TabsContents({ React.useEffect(() => { if (!containerRef.current) return; + let frameId: number | null = null; const resizeObserver = new ResizeObserver((entries) => { const newHeight = entries?.[0]?.contentRect.height; if (!newHeight) return; - requestAnimationFrame(() => { + if (frameId !== null) cancelAnimationFrame(frameId); + frameId = requestAnimationFrame(() => { + frameId = null; setHeight(newHeight); }); }); @@ -205,6 +221,7 @@ function TabsContents({ return () => { resizeObserver.disconnect(); + if (frameId !== null) cancelAnimationFrame(frameId); }; }, [children]); diff --git a/frontend/components/common/markdown/Editor.tsx b/frontend/components/common/markdown/Editor.tsx index 5daf90c5..b837499e 100644 --- a/frontend/components/common/markdown/Editor.tsx +++ b/frontend/components/common/markdown/Editor.tsx @@ -1,6 +1,6 @@ 'use client'; -import {useState, useRef, useCallback, useEffect} from 'react'; +import {useState, useRef, useCallback, useEffect, useMemo} from 'react'; import {Button} from '@/components/ui/button'; import {Textarea} from '@/components/ui/textarea'; import {Tabs, TabsList, TabsTrigger} from '@/components/ui/tabs'; @@ -37,7 +37,7 @@ export function MarkdownEditor({ const [displayLineCount, setDisplayLineCount] = useState(1); const [lineMapping, setLineMapping] = useState>([]); - const lines = value.split('\n'); + const lines = useMemo(() => value.split('\n'), [value]); const calculateLineMapping = useCallback(() => { if (!textareaRef.current || lines.length === 0) { return [{logicalLine: 1, isFirstLineOfLogicalLine: true}]; @@ -109,32 +109,42 @@ export function MarkdownEditor({ const handleInputChange = useCallback((e: React.ChangeEvent) => { onChange(e.target.value); - - setTimeout(() => { - const newLineMapping = calculateLineMapping(); - setLineMapping(newLineMapping); - setDisplayLineCount(newLineMapping.length); - }, 0); - }, [onChange, calculateLineMapping]); + }, [onChange]); useEffect(() => { + let frameId: number | null = null; const updateDisplayLines = () => { const newLineMapping = calculateLineMapping(); - setLineMapping(newLineMapping); - setDisplayLineCount(newLineMapping.length); + setLineMapping((current) => { + const unchanged = current.length === newLineMapping.length && + current.every((line, index) => + line.logicalLine === newLineMapping[index]?.logicalLine && + line.isFirstLineOfLogicalLine === newLineMapping[index]?.isFirstLineOfLogicalLine, + ); + return unchanged ? current : newLineMapping; + }); + setDisplayLineCount((current) => + current === newLineMapping.length ? current : newLineMapping.length, + ); + }; + const scheduleUpdate = () => { + if (frameId !== null) cancelAnimationFrame(frameId); + frameId = requestAnimationFrame(() => { + frameId = null; + updateDisplayLines(); + }); }; if (textareaRef.current) { - setTimeout(updateDisplayLines, 0); + scheduleUpdate(); - const resizeObserver = new ResizeObserver(() => { - setTimeout(updateDisplayLines, 0); - }); + const resizeObserver = new ResizeObserver(scheduleUpdate); resizeObserver.observe(textareaRef.current); return () => { resizeObserver.disconnect(); + if (frameId !== null) cancelAnimationFrame(frameId); }; } }, [calculateLineMapping]); @@ -150,7 +160,8 @@ export function MarkdownEditor({ } }; - setTimeout(syncStyles, 0); + const frameId = requestAnimationFrame(syncStyles); + return () => cancelAnimationFrame(frameId); } }, [value, displayLineCount]); From bcbc33e9af738a857c94c852af13c3d85c0f331f Mon Sep 17 00:00:00 2001 From: Aether_254 <67494671+Aether-254@users.noreply.github.com> Date: Mon, 21 Sep 2026 14:14:32 +0100 Subject: [PATCH 2/3] fix(motion-highlight): clear highlight bounds when no item is active The per-item effect no longer calls clearBounds, so move the reset into the parent where activeValue is owned. Bounds are cleared only when activeValue becomes null; switching between items keeps the highlight so it can animate to the new position. --- frontend/components/animate-ui/effects/motion-highlight.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/components/animate-ui/effects/motion-highlight.tsx b/frontend/components/animate-ui/effects/motion-highlight.tsx index a0b9a646..7ee41eab 100644 --- a/frontend/components/animate-ui/effects/motion-highlight.tsx +++ b/frontend/components/animate-ui/effects/motion-highlight.tsx @@ -188,6 +188,10 @@ function MotionHighlight({ else if (defaultValue !== undefined) setActiveValue(defaultValue); }, [value, defaultValue]); + React.useEffect(() => { + if (mode === 'parent' && activeValue === null) clearBounds(); + }, [mode, activeValue, clearBounds]); + const id = React.useId(); React.useEffect(() => { From 51bbdc2d816758bbdd795520936a18ed08b9a022 Mon Sep 17 00:00:00 2001 From: Chenyme <118253778+chenyme@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:55:30 +0800 Subject: [PATCH 3/3] fix(tabs): observe subtree so the active indicator follows tab switches --- .../animate-ui/effects/motion-highlight.tsx | 2 +- frontend/components/animate-ui/radix/tabs.tsx | 15 ++------------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/frontend/components/animate-ui/effects/motion-highlight.tsx b/frontend/components/animate-ui/effects/motion-highlight.tsx index 7ee41eab..6cd68ee3 100644 --- a/frontend/components/animate-ui/effects/motion-highlight.tsx +++ b/frontend/components/animate-ui/effects/motion-highlight.tsx @@ -189,7 +189,7 @@ function MotionHighlight({ }, [value, defaultValue]); React.useEffect(() => { - if (mode === 'parent' && activeValue === null) clearBounds(); + if (mode === 'parent' && !activeValue) clearBounds(); }, [mode, activeValue, clearBounds]); const id = React.useId(); diff --git a/frontend/components/animate-ui/radix/tabs.tsx b/frontend/components/animate-ui/radix/tabs.tsx index 174cfe64..c57f41fd 100644 --- a/frontend/components/animate-ui/radix/tabs.tsx +++ b/frontend/components/animate-ui/radix/tabs.tsx @@ -73,24 +73,13 @@ function TabsList({ if (!container) return; const frameId = requestAnimationFrame(getActiveValue); - const observer = new MutationObserver((mutations) => { - for (const mutation of mutations) { - if ( - mutation.type === 'attributes' && - mutation.attributeName === 'data-value' - ) { - continue; - } - getActiveValue(); - return; - } - }); + const observer = new MutationObserver(getActiveValue); observer.observe(container, { attributes: true, attributeFilter: ['data-state'], childList: true, - subtree: false, + subtree: true, }); return () => {