-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve DataForm performance #65003
base: trunk
Are you sure you want to change the base?
Improve DataForm performance #65003
Conversation
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.
To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
||
const MemoizedFieldEdit = memo( | ||
( { field, data, onChange } ) => ( | ||
<field.Edit data={ data } field={ field } onChange={ onChange } /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if instead of passing data
we pass field.getValue( { item: data } )
and we just memoize FieldEdit
simply.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the review! You raise a great point. I'm not entirely sure—could it be that the data object might be useful for more complex edit field components?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could it be that the data object might be useful for more complex edit field components?
If it's the case, the current memoization will break it because it won't update if something else changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, you're right! I'm inclined to follow with your suggestion and revisit this approach if/when needed: WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Works for me.
…erg into fix/data-form-performance
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking good to me. Can we add a "CHANGELOG" note about it. I'm guessing this changes the API of custom Edit
functions, so it's a breaking change.
I'm curious about @oandregal's thoughts here as well.
I noticed that it is necessary the callback to the memo function: the issue is the
I’m happy to close or put this PR on hold and revisit it later if you feel it’s a premature optimization. |
( { field, value, onChange } ) => ( | ||
<field.Edit value={ value } field={ field } onChange={ onChange } /> | ||
), | ||
( prevProps, nextProps ) => prevProps.value === nextProps.value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure we should be doing this though. This would prevent the component from re-rendering if field or onChange change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, true! I realize that finding a good fix might be more challenging than I initially thought. As I mentioned earlier, I'm okay with closing this PR for now, but we should definitely revisit this issue later. Large forms could eventually suffer from performance issues, so it’s worth addressing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No for me this PR is good.
- I don't expect "field" to change over time
- I'm not sure what we're doing right now with "onChange" but we can easily pass a stable reference that calls the latest prop.
So I think we can consider memoizing the fields like you did with a simple memo
call. That said, in general I like to consider metrics before doing performance improvements. If we notice a real issue, we can add a metric to code vitals.run to ensure we keep that good over time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't expect "field" to change over time
One situation where this will happen is when the elements
of one field depend on another field. Think of countries & regions situation, for example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One situation where this will happen is when the elements of one field depend on another field. Think of countries & regions situation, for example.
I don't think it's the "field" that changes in this situation, we will need access to the whole "data" though yes and not just the current item.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's the "field" that changes in this situation, we will need access to the whole "data" though yes and not just the current item.
🤔 Aren't the filter elements defined by the Field? When a user changes the country to a different one, the region field would need to recreate their element list, wouldn't it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aren't the filter elements defined by the Field? When a user changes the country to a different one, the region field would need to recreate their element list, wouldn't it?
I think we need a better way to address this use-case. Maybe the elements as a function or something like that. I think field declaration should ideally be "static" mostly.
@@ -122,7 +122,7 @@ function FormField< Item >( { | |||
/> | |||
<field.Edit | |||
key={ field.id } | |||
data={ data } | |||
value={ field.getValue( { item: data } ) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, the core change here is decoupling components from data changes. That sounds good. However, what if the value is an object or array? Wouldn't we have the same issue? I'm thinking of fields that represent a list of categories, for example.
API-wise, I'm not strongly opposed to doing something like this where the derived state (value) is calculated outside the Edit — though I don't love it. Are there alternatives we can consider that don't collocate derived state away from Edit? 🤔 Does it make sense to pass a getData
stable reference instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
However, what if the value is an object or array? Wouldn't we have the same issue? I'm thinking of fields that represent a list of categories, for example.
This has no impact, the instance of that object or array won't change when you edit another field
What?
While investigating the DataView/DataForm related to WooCommerce issue #50992, I found that the dataform component in the regular view re-renders in its entirety whenever the onChange function is triggered. To enhance performance, this PR addresses the issue by ensuring that only the specific input that has changed is re-rendered, rather than the entire component.
Why?
This PR is necessary because re-rendering the entire component on every change can lead to performance bottlenecks, especially in long forms or those with slow components. For example, I simulated a slow edit component for the title field and observed significant performance improvements with this patch compared to without it.
You can verify that even the title field component is slow, this doesn't impact the speed of the other fields.
Screen.Capture.on.2024-09-03.at.14-13-11.mp4
Screen.Capture.on.2024-09-03.at.14-14-27.mp4
Testing Instructions
packages/dataviews/src/dataform-controls/text.tsx
file adding this function at top of the file:In the
Text
component invoke this function:npm run storybook:dev
).DataForm
story.DataForm
story.Testing Instructions for Keyboard
Screenshots or screencast