Skip to content

Optimize condition and field array performance #1017

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

Merged
merged 6 commits into from
Mar 31, 2021

Conversation

Hyperkid123
Copy link
Member

fixes #1016

Description
The issue was that any field wrapped in the condition is subject to render, even if the when value did not change. This introduced improved FormSpy that listens only on a set of values in the form state. The effect of the new spy is that we no longer trigger render cycle in every condition wrapped field and by that extension skips the condition evaluation because it's not required.

See profiler snapshots and examples in the linked issue.

@Hyperkid123 Hyperkid123 added renderer React form renderer PR Technical debt labels Mar 26, 2021
@Hyperkid123 Hyperkid123 requested a review from rvsia March 26, 2021 14:40
@codecov
Copy link

codecov bot commented Mar 26, 2021

Codecov Report

Merging #1017 (c0a455c) into master (44473bc) will increase coverage by 0.26%.
The diff coverage is 100.00%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1017      +/-   ##
==========================================
+ Coverage   84.80%   85.06%   +0.26%     
==========================================
  Files         199      200       +1     
  Lines        3264     3321      +57     
  Branches     1115     1130      +15     
==========================================
+ Hits         2768     2825      +57     
  Misses        496      496              
Impacted Files Coverage Δ
...nt-component-mapper/src/field-array/field-array.js 98.33% <100.00%> (+0.02%) ⬆️
...nt-component-mapper/src/field-array/field-array.js 100.00% <100.00%> (ø)
...on-component-mapper/src/field-array/field-array.js 100.00% <100.00%> (ø)
...ui-component-mapper/src/field-array/field-array.js 100.00% <100.00%> (ø)
...f4-component-mapper/src/field-array/field-array.js 100.00% <100.00%> (ø)
...t-form-renderer/src/form-renderer/form-renderer.js 100.00% <100.00%> (ø)
...act-form-renderer/src/form-renderer/render-form.js 100.00% <100.00%> (ø)
...c/get-condition-triggers/get-condition-triggers.js 100.00% <100.00%> (ø)
...t-form-renderer/src/use-field-api/use-field-api.js 91.66% <100.00%> (+0.45%) ⬆️
...ir-component-mapper/src/field-array/field-array.js 100.00% <100.00%> (ø)
... and 1 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 750d8cc...c0a455c. Read the comment docs.

@Hyperkid123 Hyperkid123 changed the title fix(renderer): optimize condition rendering. [WIP] fix(renderer): optimize condition rendering Mar 29, 2021
@Hyperkid123
Copy link
Member Author

I have thought of one possible issue and that is we are not accepting the dot notation when constructing the reduced values object. Ill adjust the code.

@rvsia
Copy link
Contributor

rvsia commented Mar 29, 2021

As we discussed in DMs, there is the issue with an unwanted registering fields that are being created in the condition wrapper. We should make this setting opt-in and describe the caveat in the documentation, or we could update the getRegisteredFields to ignore fields created by conditions (however, the question is then how to know what fields are created by conditions or by the useFieldHook)

Or we can wait until our state manager is implemented - then it would be simple to implement FieldSpy. 😈

edit: Theoretically we can make an array in a ref (so no rerenders) and update it in the useFieldApi hooks - and then return the array in the method instead of the FF's one. 🤔 However, not sure if there could be any side-effects of this solution.

@rvsia
Copy link
Contributor

rvsia commented Mar 31, 2021

schema for checking the wizard

const wizardSchema = {
  fields: [
    {
      component: componentTypes.WIZARD,
      name: 'wizzard',
      buttonLabels: {},
      title: 'Cokoliv',
      fields: [
        { name: 'first', nextStep: ({values: {path}}) => path ? 'path1' : 'path2', fields: [{
          name: 'path',
          component: componentTypes.SWITCH,
          label: 'Path 1?'
        }]},
        {
          name: 'path1',
          nextStep: 'summary',
          fields: [{
            component: componentTypes.TEXT_FIELD,
            name: 'text1',
            label: 'text1',
            validate: [{type: 'required'}]
          }]
        },
        {
          name: 'path2',
          nextStep: 'summary',
          fields: [{
            component: componentTypes.TEXT_FIELD,
            name: 'text2',
            label: 'text2',
            validate: [{type: 'required'}]
          }]
        },
        {
          name: 'summary',
          fields: [{
            name: 'summary',
            component: componentTypes.TEXTAREA,
            label: 'summary',
            condition: {
              or: [
                {when: 'text1', isNotEmpty: true},
                {when: 'text2', isNotEmpty: true},
              ]
            }
          }]
        }
      ]
    }
  ]
};

@Hyperkid123
Copy link
Member Author

Hyperkid123 commented Mar 31, 2021

@rvsia I've added a custom field registration method. The wizard works with the new registration. There I've kept the FF getRegisteredFields under an alias.

That part is ready. I will check the field array and what might be the bottleneck here.

@@ -26,9 +26,20 @@ const FormRenderer = ({
...props
}) => {
const [fileInputs, setFileInputs] = useState([]);
const [registeredFields, setRegisteredFields] = useState({});
Copy link
Contributor

Choose a reason for hiding this comment

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

What about to use ref to not trigger any renders?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think this triggers any additional renders of the children 🤔 Let me check the profiler. But, using ref might cause some race conditions because we can't use a callback to override the value.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh yeah, it does 🤦 we don't shield the render from parent re-renders. I'm just gonna slap the ref there and we will see what it does.

Copy link
Contributor

Choose a reason for hiding this comment

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

You don't have to use a callback. It's an array, so you can modify it and the reference is still the same. And you don't have to use an object: use array with duplicates and the function will return a Set from the array.

Copy link
Member Author

Choose a reason for hiding this comment

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

OK it almost totally undid all the work. We can mimic the callback from setState which will make using ref safer.

Copy link
Contributor

Choose a reason for hiding this comment

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

So register just pushes the name into the array, unregister removes the first occurrence of the name and getRegistered returns the array without duplicates.

@Hyperkid123 Hyperkid123 force-pushed the renderer-optimization branch from ee0d6a9 to 044f6de Compare March 31, 2021 09:04

const name = internalTriggers.shift();
return (
<FieldProvider skipRegistration name={name} subscription={{ value: true }} render={({input: {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 would still use the FF field - we don't need any of our useFieldApi features (and so we don't have to use any skipRegistration)

@@ -111,6 +159,8 @@ SingleField.propTypes = {
resolveProps: PropTypes.func
};

const renderForm = (fields) => fields.map((field) => (Array.isArray(field) ? renderForm(field) : <SingleField key={field.name} {...field} />));
const renderForm = (fields) => {
return fields.map((field) => (Array.isArray(field) ? renderForm(field) : <SingleField key={field.name} {...field} />));
Copy link
Contributor

Choose a reason for hiding this comment

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

return block

@Hyperkid123 Hyperkid123 force-pushed the renderer-optimization branch from 044f6de to d5a7ed8 Compare March 31, 2021 09:32
@Hyperkid123 Hyperkid123 force-pushed the renderer-optimization branch from d5a7ed8 to 4fa3cd5 Compare March 31, 2021 09:56
@Hyperkid123 Hyperkid123 changed the title [WIP] fix(renderer): optimize condition rendering Optimize condition and field array performance Mar 31, 2021
@Hyperkid123
Copy link
Member Author

@rvsia I think it's ready. Feel free to dive into the code.

@@ -69,7 +69,8 @@
"react-dom": ">=16.13.0"
},
"dependencies": {
"@data-driven-forms/common": "*"
"@data-driven-forms/common": "*",
"lodash": "^4.17.21"
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't lodash dependency of form-renderer?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, but now lodash is a direct dependency of the package and should be listed.

Comment on lines +34 to +43
const internalRegisterField = (name) => {
setRegisteredFields(prev => prev[name] ? ({...prev, [name]: prev[name] + 1}) : ({...prev, [name]: 1}));
};

const internalUnRegisterField = (name) => {
setRegisteredFields(({[name]: currentField, ...prev}) => currentField && currentField > 1 ? ({[name]: currentField - 1, ...prev}) : prev);
};

const internalGetRegisteredFields = () => Object.entries(registeredFields.current).reduce((acc, [name, value]) => value > 0 ? [...acc, name] : acc, []);

Copy link
Contributor

Choose a reason for hiding this comment

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

What about to do what I proposed (this seems unnecessary complex to me):

  const registeredFields = useRef([]);

register:

const internalRegisterField = (name) => registeredFields.current.push(name)

unregister:

function removeElement(array, elem) {
    var index = array.indexOf(elem);
    if (index > -1) {
        array.splice(index, 1);
    }
}

const internalUnRegisterField = (name) => removeElement(registeredFields.current, name)

internalGetRegisteredFields

const internalGetRegisteredFields = () => new Set(registeredFields.current)

Copy link
Member Author

Choose a reason for hiding this comment

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

There is a couple of potential issues. First, I don't like the idea of having the same item multiple times inside the array. Using an object is slightly more resource-efficient than looking for indexes.

Another problem might be the usage of push which can create an out-of-sync structure when we re-create the array (splice in removeElement). Theoretically, we can push into an array that no longer exists.

Also this:

const internalGetRegisteredFields = () => new Set(registeredFields.current)

Would return an instance of a set instead of an array. We would need either Array.from or [...x] destructor to actually get an array to preserve current API. So we need some value transformation anyway.

<ConditionTriggerDetector triggers={triggers} condition={condition} field={field}>
{children}
</ConditionTriggerDetector>
);
Copy link
Contributor

Choose a reason for hiding this comment

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

strange formatting here

@Hyperkid123 Hyperkid123 merged commit 75096be into master Mar 31, 2021
@Hyperkid123 Hyperkid123 deleted the renderer-optimization branch March 31, 2021 13:07
@Hyperkid123
Copy link
Member Author

🎉 This PR is included in version 3.3.2 🎉

The release is available on

Demo can be found here!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[RENDERER] Block unnecessary condition and hide field renders
2 participants