From 96f72be88ecd5e8880e7c113ae132835b019e24c Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Mon, 6 Apr 2026 18:15:03 -0700 Subject: [PATCH] Debounce MutationObserver and selectionchange in MarkdownEditor The MutationObserver for mention decoration fired decorateProjectMentions synchronously on every character change, querying all links and writing to the DOM on each keystroke. The selectionchange listener similarly ran checkMention (which calls getBoundingClientRect) on every cursor move. Coalesce both into single requestAnimationFrame callbacks so multiple rapid mutations/selections batch into one decoration pass per frame. Co-Authored-By: Paperclip --- ui/src/components/MarkdownEditor.tsx | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/ui/src/components/MarkdownEditor.tsx b/ui/src/components/MarkdownEditor.tsx index 223b5755631..d9edaf6c175 100644 --- a/ui/src/components/MarkdownEditor.tsx +++ b/ui/src/components/MarkdownEditor.tsx @@ -509,11 +509,21 @@ export const MarkdownEditor = forwardRef // also fires after typing (e.g. space to dismiss). const onInput = () => requestAnimationFrame(checkMention); - document.addEventListener("selectionchange", checkMention); + let selRafId: number | null = null; + const onSelectionChange = () => { + if (selRafId !== null) return; + selRafId = requestAnimationFrame(() => { + selRafId = null; + checkMention(); + }); + }; + + document.addEventListener("selectionchange", onSelectionChange); el?.addEventListener("input", onInput, true); return () => { - document.removeEventListener("selectionchange", checkMention); + document.removeEventListener("selectionchange", onSelectionChange); el?.removeEventListener("input", onInput, true); + if (selRafId !== null) cancelAnimationFrame(selRafId); }; }, [checkMention, mentions, slashCommands.length]); @@ -540,15 +550,23 @@ export const MarkdownEditor = forwardRef const editable = containerRef.current?.querySelector('[contenteditable="true"]'); if (!editable) return; decorateProjectMentions(); + let rafId: number | null = null; const observer = new MutationObserver(() => { - decorateProjectMentions(); + if (rafId !== null) return; + rafId = requestAnimationFrame(() => { + rafId = null; + decorateProjectMentions(); + }); }); observer.observe(editable, { subtree: true, childList: true, characterData: true, }); - return () => observer.disconnect(); + return () => { + observer.disconnect(); + if (rafId !== null) cancelAnimationFrame(rafId); + }; }, [decorateProjectMentions]); const selectMention = useCallback(