From c2472ac471cb51a7c259153c33ace2b8c7f2e087 Mon Sep 17 00:00:00 2001 From: Philipp Schneider <47689073+philipp-tailor@users.noreply.github.com> Date: Fri, 11 Apr 2025 14:36:16 +0200 Subject: [PATCH] perf: debounce rich text editor field value updates Follow-up work to #12046: Debounces rich text editor `setState()` calls. Using `requestIdleCallback` leads to better scheduling of change event handling in the rich text editor, but on CPU-starved clients, this leads to a large backlog of unprocessed idle callbacks. Since idle callbacks are called by the browser in submission order, the latest callback will be processed last, potentially leading to large time delays between a user typing, and the form state having been updated. An example: When a user types "I", and the change events for the character "I" is scheduled to happen in the next browser idle time, but then the user goes on to type "love Payload", there will be 12 more callbacks scheduled. On a slow system it's preferable if the browser right away only processes the event that has the full editor state "I love Payload", instead of only processing that after 11 other idle callbacks. So this code change keeps track when requesting an idle callback and cancels the previous one when a new change event with an updated editor state occurrs. --- packages/richtext-lexical/src/field/Field.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/richtext-lexical/src/field/Field.tsx b/packages/richtext-lexical/src/field/Field.tsx index 65edb6ede58..cc73bb3e49c 100644 --- a/packages/richtext-lexical/src/field/Field.tsx +++ b/packages/richtext-lexical/src/field/Field.tsx @@ -12,7 +12,7 @@ import { useField, } from '@payloadcms/ui' import { mergeFieldStyles } from '@payloadcms/ui/shared' -import React, { useCallback, useEffect, useMemo, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { ErrorBoundary } from 'react-error-boundary' import type { SanitizedClientEditorConfig } from '../lexical/config/types.js' @@ -117,6 +117,8 @@ const RichTextComponent: React.FC< const pathWithEditDepth = `${path}.${editDepth}` + const dispatchFieldUpdateTask = useRef(undefined) + const updateFieldValue = (editorState: EditorState) => { const newState = editorState.toJSON() prevValueRef.current = newState @@ -126,7 +128,19 @@ const RichTextComponent: React.FC< const handleChange = useCallback( (editorState: EditorState) => { if (typeof window.requestIdleCallback === 'function') { - requestIdleCallback(() => updateFieldValue(editorState)) + // Cancel earlier scheduled value updates, + // so that a CPU-limited event loop isn't flooded with n callbacks for n keystrokes into the rich text field, + // but that there's only ever the latest one state update + // dispatch task, to be executed with the next idle time, + // or the deadline of 500ms. + if (typeof window.cancelIdleCallback === 'function' && dispatchFieldUpdateTask.current) { + cancelIdleCallback(dispatchFieldUpdateTask.current) + } + // Schedule the state update to happen the next time the browser has sufficient resources, + // or the latest after 500ms. + dispatchFieldUpdateTask.current = requestIdleCallback(() => updateFieldValue(editorState), { + timeout: 500, + }) } else { updateFieldValue(editorState) }