perf(ui): useAsTitle field lags on slow cpu (#12436)
When running the Payload admin panel on a machine with a slower CPU, form state lags significantly and can become nearly unusable or even crash when interacting with the document's `useAsTitle` field. Here's an example: https://github.com/user-attachments/assets/3535fa99-1b31-4cb6-b6a8-5eb9a36b31b7 #### Why this happens The reason for this is that entire React component trees are re-rendering on every keystroke of the `useAsTitle` field, twice over. Here's a breakdown of the flow: 1. First, we dispatch form state events to the form context. Only the components that are subscribed to form state re-render when this happens (good). 2. Then, we sync the `useAsTitle` field to the document info provider, which lives outside the form. Regardless of whether its children need to be aware of the document title, all components subscribed to the document info context will re-render (there are many, including the form itself). Given how far up the rendering tree the document info provider is, its rendering footprint, and the rate of speed at which these events are dispatched, this is resource intensive. #### What is the fix The fix is to isolate the document's title into it's own context. This way only the components that are subscribed to specifically this context will re-render as the title changes. Here's the same test with the same CPU throttling, but no lag: https://github.com/user-attachments/assets/c8ced9b1-b5f0-4789-8d00-a2523d833524
This commit is contained in:
@@ -251,6 +251,57 @@ test.describe('Form State', () => {
|
||||
).toHaveValue('This is a default value')
|
||||
})
|
||||
|
||||
// TODO: This test is not very reliable but would be really nice to have
|
||||
test.skip('should not lag on slow CPUs', async () => {
|
||||
await page.goto(postsUrl.create)
|
||||
|
||||
await expect(page.locator('#field-title')).toBeEnabled()
|
||||
|
||||
const cdpSession = await context.newCDPSession(page)
|
||||
|
||||
await cdpSession.send('Emulation.setCPUThrottlingRate', { rate: 25 })
|
||||
|
||||
// Start measuring input and render times
|
||||
await page.evaluate(() => {
|
||||
const inputField = document.querySelector('#field-title') as HTMLInputElement
|
||||
const logs: Record<string, { elapsedTime: number }> = {}
|
||||
|
||||
inputField.addEventListener('input', (event) => {
|
||||
const startTime = performance.now()
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const endTime = performance.now()
|
||||
const elapsedTime = endTime - startTime
|
||||
logs[event.target?.value] = { elapsedTime }
|
||||
})
|
||||
})
|
||||
|
||||
window.getLogs = () => logs
|
||||
})
|
||||
|
||||
const text = 'This is a test string to measure input lag.'
|
||||
|
||||
await page.locator('#field-title').pressSequentially(text, { delay: 0 })
|
||||
|
||||
const logs: Record<string, { elapsedTime: number }> = await page.evaluate(() =>
|
||||
window.getLogs(),
|
||||
)
|
||||
console.log('Logs:', logs)
|
||||
|
||||
const lagTimes = Object.values(logs).map((log) => log.elapsedTime || 0)
|
||||
|
||||
console.log('Lag times:', lagTimes)
|
||||
|
||||
const maxInputLag = Math.max(...lagTimes)
|
||||
const allowedThreshold = 50
|
||||
|
||||
expect(maxInputLag).toBeLessThanOrEqual(allowedThreshold)
|
||||
|
||||
// Reset CPU throttling
|
||||
await cdpSession.send('Emulation.setCPUThrottlingRate', { rate: 1 })
|
||||
await cdpSession.detach()
|
||||
})
|
||||
|
||||
describe('Throttled tests', () => {
|
||||
let cdpSession: CDPSession
|
||||
|
||||
|
||||
Reference in New Issue
Block a user