Skip to content
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

Open
wants to merge 6 commits into
base: trunk
Choose a base branch
from

Conversation

gigitux
Copy link
Contributor

@gigitux gigitux commented Sep 3, 2024

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.

Before After
Screen.Capture.on.2024-09-03.at.14-13-11.mp4
Screen.Capture.on.2024-09-03.at.14-14-27.mp4

Testing Instructions

  1. Checkout trunk
  2. Edit the packages/dataviews/src/dataform-controls/text.tsx file adding this function at top of the file:
function slowFunction() {
	// Simulate a CPU-intensive task
	const start = performance.now();
	while ( performance.now() - start < 1000 ) {
		// This loop runs for 1 second, simulating a slow computation
	}
	return;
}

In the Text component invoke this function:

export default function Text< Item >( {
	data,
	field,
	onChange,
	hideLabelFromVision,
}: DataFormControlProps< Item > ) {
	const { id, label, placeholder } = field;
	const value = field.getValue( { item: data } );

	slowFunction();

	const onChangeControl = useCallback(
		( newValue: string ) =>
			onChange( {
				[ id ]: newValue,
			} ),
		[ id, onChange ]
	);
  1. Run Storybook ( npm run storybook:dev).
  2. Check the DataForm story.
  3. Try to update the title input.
  4. Ensure that the title input is slow.
  5. Edit other fields.
  6. Expect that other fields are slow.
  7. Checkout this branch (keep the changes)
  8. Check the DataForm story.
  9. Try to update the title input.
  10. Ensure that the title input is slow.
  11. Edit other fields.
  12. Expect that other fields aren't slow

Testing Instructions for Keyboard

Screenshots or screencast

Copy link

github-actions bot commented Sep 3, 2024

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 props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: gigitux <gigitux@git.wordpress.org>
Co-authored-by: youknowriad <youknowriad@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@cbravobernal cbravobernal added [Type] Performance Related to performance efforts [Package] DataViews /packages/dataviews labels Sep 3, 2024

const MemoizedFieldEdit = memo(
( { field, data, onChange } ) => (
<field.Edit data={ data } field={ field } onChange={ onChange } />
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor Author

@gigitux gigitux Sep 3, 2024

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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works for me.

Copy link
Contributor

@youknowriad youknowriad left a 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.

@gigitux
Copy link
Contributor Author

gigitux commented Sep 4, 2024

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 noticed that it is necessary the callback to the memo function: the issue is the field that is an object.

I'm curious about @oandregal's thoughts here as well.

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
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Member

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.

Copy link
Contributor

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.

Copy link
Member

@oandregal oandregal Sep 12, 2024

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?

Copy link
Contributor

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 } ) }
Copy link
Member

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?

Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Package] DataViews /packages/dataviews [Type] Performance Related to performance efforts
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants