perf: actually debounce rich text editor field value updates to only process latest state (#12086)
Follow-up work to #12046, which was misnamed. It improved UI responsiveness of the rich text field on CPU-limited clients, but didn't actually reduce work by debouncing. It only improved scheduling. Using `requestIdleCallback` lead 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 occurs.
This commit is contained in:
committed by
GitHub
parent
faa7794cc7
commit
1d5d96d2c3
@@ -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'
|
||||
@@ -116,16 +116,30 @@ const RichTextComponent: React.FC<
|
||||
|
||||
const pathWithEditDepth = `${path}.${editDepth}`
|
||||
|
||||
const updateFieldValue = (editorState: EditorState) => {
|
||||
const newState = editorState.toJSON()
|
||||
prevValueRef.current = newState
|
||||
setValue(newState)
|
||||
}
|
||||
const dispatchFieldUpdateTask = useRef<number>(undefined)
|
||||
|
||||
const handleChange = useCallback(
|
||||
(editorState: EditorState) => {
|
||||
const updateFieldValue = (editorState: EditorState) => {
|
||||
const newState = editorState.toJSON()
|
||||
prevValueRef.current = newState
|
||||
setValue(newState)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user