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 <noreply@paperclip.ing>
This commit is contained in:
Devin FoleyandPaperclip committed 2026-04-06 18:15:03 -07:00
1 parent 4a6d3a0238
commit 96f72be88e
1 file changed
+22 -4
+22 -4
View File
@@ -509,11 +509,21 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
// 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<MarkdownEditorRef, MarkdownEditorProps>
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(