perf(ui): significantly optimize form state component rendering, up to 96% smaller and 75% faster (#11946)

Significantly optimizes the component rendering strategy within the form
state endpoint by precisely rendering only the fields that require it.
This cuts down on server processing and network response sizes when
invoking form state requests **that manipulate array and block rows
which contain server components**, such as rich text fields, custom row
labels, etc. (results listed below).

Here's a breakdown of the issue:

Previously, when manipulating array and block fields, _all_ rows would
render any server components that might exist within them, including
rich text fields. This means that subsequent changes to these fields
would potentially _re-render_ those same components even if they don't
require it.

For example, if you have an array field with a rich text field within
it, adding the first row would cause the rich text field to render,
which is expected. However, when you add a second row, the rich text
field within the first row would render again unnecessarily along with
the new row.

This is especially noticeable for fields with many rows, where every
single row processes its server components and returns RSC data. And
this does not only affect nested rich text fields, but any custom
component defined on the field level, as these are handled in the same
way.

The reason this was necessary in the first place was to ensure that the
server components receive the proper data when they are rendered, such
as the row index and the row's data. Changing one of these rows could
cause the server component to receive the wrong data if it was not
freshly rendered.

While this is still a requirement that rows receive up-to-date props, it
is no longer necessary to render everything.

Here's a breakdown of the actual fix:

This change ensures that only the fields that are actually being
manipulated will be rendered, rather than all rows. The existing rows
will remain in memory on the client, while the newly rendered components
will return from the server. For example, if you add a new row to an
array field, only the new row will render its server components.

To do this, we send the path of the field that is being manipulated to
the server. The server can then use this path to determine for itself
which fields have already been rendered and which ones need required
rendering.

## Results

The following results were gathered by booting up the `form-state` test
suite and seeding 100 array rows, each containing a rich text field. To
invoke a form state request, we navigate to a document within the
"posts" collection, then add a new array row to the list. The result is
then saved to the file system for comparison.

| Test Suite | Collection | Number of Rows | Before | After | Percentage
Change |
|------|------|---------|--------|--------|--------|
| `form-state` | `posts` | 101 | 1.9MB / 266ms | 80KB / 70ms | ~96%
smaller / ~75% faster |

---------

Co-authored-by: James <james@trbl.design>
Co-authored-by: Alessio Gravili <alessio@gravili.de>
This commit is contained in:
Jacob Fletcher
2025-04-03 12:27:14 -04:00
committed by GitHub
parent 8880d705e3
commit e87521a376
26 changed files with 585 additions and 295 deletions

View File

@@ -2,15 +2,38 @@ import type { Page } from '@playwright/test'
import { expect } from '@playwright/test'
/**
* A helper function to assert the body of a network request.
* This is useful for reading the body of a request and testing whether it is correct.
* For example, if you have a form that submits data to an API, you can use this function to
* assert that the data being sent is correct.
* @param page The Playwright page
* @param options Options
* @param options.action The action to perform that will trigger the request
* @param options.expect A function to run after the request is made to assert the request body
* @returns The request body
* @example
* const requestBody = await assertRequestBody(page, {
* action: page.click('button'),
* expect: (requestBody) => expect(requestBody.foo).toBe('bar')
* })
*/
export const assertRequestBody = async <T>(
page: Page,
options: {
action: Promise<void> | void
expect?: (requestBody: T) => boolean | Promise<boolean>
requestMethod?: string
url: string
},
): Promise<T | undefined> => {
const [request] = await Promise.all([
page.waitForRequest((request) => request.method() === 'POST'), // Adjust condition as needed
page.waitForRequest((request) =>
Boolean(
request.url().startsWith(options.url) &&
(request.method() === options.requestMethod || 'POST'),
),
),
await options.action,
])

View File

@@ -0,0 +1,86 @@
import type { Page } from '@playwright/test'
import { expect } from '@playwright/test'
function parseRSC(rscText: string) {
// Next.js streams use special delimiters like "\n"
const chunks = rscText.split('\n').filter((line) => line.trim() !== '')
// find the chunk starting with '1:', remove the '1:' prefix and parse the rest
const match = chunks.find((chunk) => chunk.startsWith('1:'))
if (match) {
const jsonString = match.slice(2).trim()
if (jsonString) {
try {
return JSON.parse(jsonString)
} catch (err) {
console.error('Failed to parse JSON:', err)
}
}
}
return null
}
/**
* A helper function to assert the response of a network request.
* This is useful for reading the response of a request and testing whether it is correct.
* For example, if you have a form that submits data to an API, you can use this function to
* assert that the data sent back is correct.
* @param page The Playwright page
* @param options Options
* @param options.action The action to perform that will trigger the request
* @param options.expect A function to run after the request is made to assert the response body
* @param options.url The URL to match in the network requests
* @returns The request body
* @example
* const responseBody = await assertResponseBody(page, {
* action: page.click('button'),
* expect: (responseBody) => expect(responseBody.foo).toBe('bar')
* })
*/
export const assertResponseBody = async <T>(
page: Page,
options: {
action: Promise<void> | void
expect?: (requestBody: T) => boolean | Promise<boolean>
requestMethod?: string
responseContentType?: string
url?: string
},
): Promise<T | undefined> => {
const [response] = await Promise.all([
page.waitForResponse((response) =>
Boolean(
response.url().includes(options.url || '') &&
response.status() === 200 &&
response
.headers()
['content-type']?.includes(options.responseContentType || 'application/json'),
),
),
await options.action,
])
if (!response) {
throw new Error('No response received')
}
const responseBody = await response.text()
const responseType = response.headers()['content-type']?.split(';')[0]
let parsedBody: T = undefined as T
if (responseType === 'text/x-component') {
parsedBody = parseRSC(responseBody)
} else if (typeof responseBody === 'string') {
parsedBody = JSON.parse(responseBody) as T
}
if (typeof options.expect === 'function') {
expect(await options.expect(parsedBody)).toBeTruthy()
}
return parsedBody
}