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:
@@ -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,
|
||||
])
|
||||
|
||||
|
||||
86
test/helpers/e2e/assertResponseBody.ts
Normal file
86
test/helpers/e2e/assertResponseBody.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user