Skip to content

Commit 7f86b5b

Browse files
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.
1 parent 3287f70 commit 7f86b5b

File tree

1 file changed

+16
-2
lines changed

1 file changed

+16
-2
lines changed

Diff for: packages/richtext-lexical/src/field/Field.tsx

+16-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
useField,
1313
} from '@payloadcms/ui'
1414
import { mergeFieldStyles } from '@payloadcms/ui/shared'
15-
import React, { useCallback, useEffect, useMemo, useState } from 'react'
15+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
1616
import { ErrorBoundary } from 'react-error-boundary'
1717

1818
import type { SanitizedClientEditorConfig } from '../lexical/config/types.js'
@@ -117,6 +117,8 @@ const RichTextComponent: React.FC<
117117

118118
const pathWithEditDepth = `${path}.${editDepth}`
119119

120+
const dispatchFieldUpdateTask = useRef()
121+
120122
const updateFieldValue = (editorState: EditorState) => {
121123
const newState = editorState.toJSON()
122124
prevValueRef.current = newState
@@ -126,7 +128,19 @@ const RichTextComponent: React.FC<
126128
const handleChange = useCallback(
127129
(editorState: EditorState) => {
128130
if (typeof window.requestIdleCallback === 'function') {
129-
requestIdleCallback(() => updateFieldValue(editorState))
131+
// Cancel earlier scheduled value updates,
132+
// so that a CPU-limited event loop isn't flooded with n callbacks for n keystrokes into the rich text field,
133+
// but that there's only ever the latest one state update
134+
// dispatch task, to be executed with the next idle time,
135+
// or the deadline of 500ms.
136+
if (typeof window.cancelIdleCallback === 'function' && dispatchFieldUpdateTask.current) {
137+
cancelIdleCallback(dispatchFieldUpdateTask.current)
138+
}
139+
// Schedule the state update to happen the next time the browser has sufficient resources,
140+
// or the latest after 500ms.
141+
dispatchFieldUpdateTask.current = requestIdleCallback(() => updateFieldValue(editorState), {
142+
timeout: 500,
143+
})
130144
} else {
131145
updateFieldValue(editorState)
132146
}

0 commit comments

Comments
 (0)