perf: defer RichTextInput formatting toolbar invalidation to rAF

Defer toolbar Svelte invalidation to requestAnimationFrame when rich text
and the formatting toolbar are enabled, so reconciliation does not run in
the same turn as ProseMirror DOM updates.

Cancel a pending frame when the toolbar is disabled or on destroy, avoiding
stale callbacks and unnecessary invalidation after toggling settings.

Made-with: Cursor
This commit is contained in:
Jacob Leksan 2026-04-22 13:29:33 -04:00
parent f162d4de90
commit 259b54aa39

View file

@ -302,6 +302,25 @@
let bubbleMenuElement: Element | null = null;
let element: Element | null = null;
/** Defer FormattingButtons refresh to after layout so we don't interleave Svelte DOM work with ProseMirror's updateStateInner (reduces forced reflow). */
let toolbarInvalidateRaf = 0;
const scheduleToolbarInvalidation = () => {
if (!richText || !showFormattingToolbar) {
if (toolbarInvalidateRaf) {
cancelAnimationFrame(toolbarInvalidateRaf);
toolbarInvalidateRaf = 0;
}
return;
}
if (toolbarInvalidateRaf) cancelAnimationFrame(toolbarInvalidateRaf);
toolbarInvalidateRaf = requestAnimationFrame(() => {
toolbarInvalidateRaf = 0;
if (!editor || editor.isDestroyed) return;
editor = editor;
});
};
const options = {
throwOnError: false
};
@ -855,8 +874,7 @@
content: collaboration ? undefined : content,
autofocus: messageInput ? true : false,
onTransaction: () => {
// force re-render so `editor.isActive` works as expected
editor = editor;
scheduleToolbarInvalidation();
if (!editor) return;
htmlValue = editor.getHTML();
@ -1223,6 +1241,11 @@
});
onDestroy(() => {
if (toolbarInvalidateRaf) {
cancelAnimationFrame(toolbarInvalidateRaf);
toolbarInvalidateRaf = 0;
}
if (provider) {
provider.destroy();
}