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) }