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

Using styled components with formik is incredibly slow #1026

Closed
mjangir opened this issue Oct 20, 2018 · 43 comments
Closed

Using styled components with formik is incredibly slow #1026

mjangir opened this issue Oct 20, 2018 · 43 comments

Comments

@mjangir
Copy link

mjangir commented Oct 20, 2018

I'm just playing with formik library and found performance issue with custom component in Field tag when the custom component uses styled components heavily.

When I keep pressing the key in an input box which is wrapped in multiple hierarchy of styled components, it causes huge lagging but instead of that, if I use simple divs, it is comparatively faster.

@joycollector
Copy link

@mjangir We are having the same issues. Using debounce to handle this.

@yashaRO
Copy link

yashaRO commented Nov 5, 2018

I may not have as many styles as you do, but I do have all custom components. I saw this in another thread and it turned out to be true (runs as normal/faster in production)
#671 (comment)

@johnmcdowall
Copy link

I'm seeing the same issue with one input in a form which is a styled component - pressing and holding any key causes a large and noticeable lag

@johnmcdowall
Copy link

Incidentally, I've tried using <FastField /> and it hasn't made any difference.

@f1yn
Copy link

f1yn commented Nov 16, 2018

@joycollector, where are you placing the debounce?

@joshperrin
Copy link

joshperrin commented Nov 17, 2018

I am having this issue as well. I tried <FastField /> with no success either.

I was thinking I may have to use vanilla CSS with Formik to fix this bug.

@devjones
Copy link

I've had similar major performance issues with relatively short forms: a custom drop down and 4-5 custom input fields. I'm using tailwind.js (similar to styled components) and Yup for validation.

@dgopsq
Copy link

dgopsq commented Nov 21, 2018

Same issue here, I can confirm that production is not affected. It lags only in dev mode.

@jaredpalmer
Copy link
Owner

That’s interesting. Can you / someone reproduce this behavior in a sandbox?

@dgopsq
Copy link

dgopsq commented Nov 21, 2018

I haven't check, but trying around a bit I think that production is affected too, but way less than dev.
BTW I solved my problem setting the form value from onBlur instead of onChange.

@idanhen
Copy link

idanhen commented Nov 21, 2018

Same issue here, production is effected - but way less

@grachet
Copy link

grachet commented Nov 29, 2018

same but fastfield helped and in prod it's acceptable

@seeden
Copy link

seeden commented Dec 3, 2018

same issue.

@jaredpalmer
Copy link
Owner

This should be fixed with 1.4.0. https:/jaredpalmer/formik/releases/tag/v1.4.0

@devjones
Copy link

devjones commented Dec 9, 2018 via email

@johnmcdowall
Copy link

Hmm, not seeing any improvement on 1.4.0 for me. A single key press is causing 2 renders:

screen shot 2018-12-13 at 5 02 29 pm

This is the code for the form:

import React, { PureComponent } from 'react';
import { css } from 'emotion';

import { Formik, Form } from 'formik';
import * as Yup from 'yup';
import { Flex, Button, Input, InputGroup } from '@livery/Components';

const FlexForm = css`
  display: flex;
  flex-direction: column;
  width: 60%;
  padding: 2rem 0;
`;

const schema = Yup.object().shape({
  display_name: Yup.string()
    .min(2, 'Display name is too Short!')
    .max(50, 'Display name is too Long!')
    .required('Required'),
  statement_descriptor: Yup.string()
    .min(2, 'Too Short!')
    .max(50, 'Too Long!')
    .required('Required'),
});

export default class StripeProductEditForm extends PureComponent {
  constructor(props) {
    super(props);

    this.state = {
      initialValues: {
        display_name:
          (props.formData && props.formData.stripe_product.display_name) || '',
        statement_descriptor:
          (props.formData &&
            props.formData.stripe_product.statement_descriptor) ||
          '',
        active:
          (props.formData && props.formData.stripe_product.active) || false,
      },
    };
  }

  render() {
    return (
      <Flex column align="center">
        <Formik
          initialValues={this.state.initialValues}
          validationSchema={schema}
          onSubmit={this.props.onSubmit}
          render={({ values, errors, touched, handleChange, handleBlur }) => (
            <Form className={FlexForm}>
              <InputGroup>
                <Input
                  name="display_name"
                  label="Display Name"
                  value={values.display_name}
                  onBlur={handleBlur}
                  onChange={handleChange}
                  type="text"
                  errorMessage={
                    errors.display_name &&
                    touched.display_name &&
                    errors.display_name
                  }
                  my={2}
                  py={3}
                  px={3}
                />

                <Input
                  name="statement_descriptor"
                  label="Statement Descriptor"
                  value={values.statement_descriptor}
                  onBlur={handleBlur}
                  onChange={handleChange}
                  type="text"
                  errorMessage={
                    errors.statement_descriptor &&
                    touched.statement_descriptor &&
                    errors.statement_descriptor
                  }
                  my={2}
                  py={3}
                  px={3}
                />
              </InputGroup>
              <InputGroup mt={3}>
                <Flex justify="space-between">
                  {this.props.isEditing && (
                    <React.Fragment>
                      <Button
                        error
                        mr={'auto'}
                        py={3}
                        px={4}
                        onClick={() => {
                          this.props.onDestroy(
                            this.props.formData.stripe_product.id,
                          );
                        }}
                      >
                        Delete Product
                      </Button>
                      {!this.props.formData.stripe_product.active && (
                        <Button
                          ml={'auto'}
                          mr={'auto'}
                          onClick={() => {
                            this.props.onActivate(
                              this.props.formData.stripe_product.id,
                            );
                          }}
                        >
                          Activate Product
                        </Button>
                      )}
                      <Button
                        ml={'auto'}
                        mr={'auto'}
                        onClick={() => {
                          this.props.onSync(
                            this.props.formData.stripe_product.id,
                          );
                        }}
                      >
                        Sync Product
                      </Button>
                    </React.Fragment>
                  )}
                  <Button
                    success
                    ml={'auto'}
                    py={3}
                    px={4}
                    name="create"
                    type="submit"
                  >
                    {this.props.isEditing ? 'Update' : 'Create'} Product
                  </Button>
                </Flex>
              </InputGroup>
            </Form>
          )}
        />
      </Flex>
    );
  }
}

I appreciate this is not very helpful without a repro, will try and get one up and running this weekend.

@johnmcdowall
Copy link

@jaredpalmer Awesome! So I just upgraded to 1.4.1 and the good news is that the double re-render for a single key press is fixed, and the form input speed is now acceptable on small forms (3 or less inputs).

The bad news is that on forms with 5 or more inputs, the performance issue remains (typing is slow, pressing and holding a key freezes the UI). Will investigate more.

@seeden
Copy link

seeden commented Dec 15, 2018

thanks @johnmcdowall I have same issue :(

@rvanlaarhoven
Copy link

rvanlaarhoven commented Dec 19, 2018

I'm also still experiencing the same issues after upgrading to 1.4.1.

In my case the unnecessary re-renders are really limited to the components using styled-components. But, it only effects styled-components that are within the scope of the formik's <Formik /> component.
After replacing all styled-components with simple div's within the form the issue was resolved and only affected fields were re rendering like expected.

@baleeds
Copy link

baleeds commented Dec 20, 2018

Hi @jaredpalmer for me there is a marked improvement in 1.4.1, so thank you for that. I went from 3 renders to 2, which I suppose made it 33% faster. Overall, I think the onus is on styled components more than on you.

@johnmcdowall
Copy link

@baleeds Are you using styled-components in your Formik forms? Because this performance issue didn't happen for me when using other React form libraries, or just standard forms, so there would seem to be a Formik specific issue here...

@rvanlaarhoven
Copy link

Apparently styled-components are always rerendering if the parent component triggers a rerender (See styled-components/styled-components#1723). In my case, I have quite a lot of styled components inside the form, which are all being rerendered as soon as a value changes, since they're stored in the form's state.

So the solution probably lies in the use of wrapper components (e.g. PureComponent's) around the parts that use styled components to avoid rerenders manually.

@baleeds
Copy link

baleeds commented Dec 21, 2018

@johnmcdowall we are using styled components throughout the whole app, which includes inside of formik forms. My observation is that styled components are slow because they cause things that don't need to be components to be components, which means more render functions run (in our case, WAY more, since almost all html elements are SC).
This is compounded by the multiple renders on each keystroke of formik. Anything that causes multiple renders would multiply the issue.

@jaredpalmer
Copy link
Owner

jaredpalmer commented Dec 21, 2018

@baleeds is correct. styled-components also hooks every single component into context for theming...slowing down things even more.

@johnmcdowall
Copy link

@jaredpalmer Just an update: after updating a bunch of libs and tweaking my base style class things seem to be pretty rocking now, no noticeable lags for me anymore 👍

@johnmcdowall
Copy link

@baleeds Yeah exactly, I used the profile as well to narrow down that my base style entity was causing too many re-renders (along with the previous Formik [now fixed] multiple re-renders).

@armand1m
Copy link

@stale stale bot removed the stale label Mar 16, 2019
@rohanBagchi
Copy link

Fastfield did not work for me too. Form has ~20 fields and the lag was noticeable.
This is how I solved it [link to gist]

import React, { useState, useEffect } from "react";
import toString from 'lodash/toString';

interface ChildrenPropType {
    handleChange: (e: any) => void,
    value: string
}

interface PropType {
    fieldData: any,
    getFieldUniqueId: (data: any) => string,
    getFieldValue: (data: any) => string,
    children: (data: ChildrenPropType) => JSX.Element
}

const Inner = (props: PropType) => {
    const [inner, setInner] = useState("");
    const [existingItemId, setExistingItemId] = useState("");

    useEffect(() => {
        const {
            fieldData,
            getFieldValue,
            getFieldUniqueId
        } = props;
        if (!fieldData) return;

        const initialValue = getFieldValue(fieldData) || "";
        const fieldUniqueId: string = toString(getFieldUniqueId(fieldData));

        if (!existingItemId || !existingItemId.length || existingItemId !== fieldUniqueId) {
            setInner(initialValue);
            setExistingItemId(fieldUniqueId);
        }
    });

    const handleChange = (e: any) => setInner(e.target.value);

    return props.children({ handleChange, value: inner })
};

export default Inner;

Now to use it, [link to gist]

const innerFieldData = {
    id: props.selectedItem.id,
    ...values, // formik's form field values
};

const getFieldUniqueId = (data: any) => data.id;

<InnerField
    fieldData={innerFieldData}
    getFieldValue={(innerFieldData: any) => innerFieldData['itemDescription']}
    getFieldUniqueId={getFieldUniqueId}
>
    {({ handleChange: innerHandleChange, value }: any) => {
        return (
            <TextArea
                placeholder='DESCRIPTION'
                name="itemDescription"
                value={value}
                onBlur={(e: any) => {
                    handleBlur(e); // formik's handleBlur
                    handleChange(e); // formik's handleChange
                }}
                onChange={innerHandleChange}
            />
        )
    }}
</InnerField>

That way sync happens only when user blurs and the field maintains it's state internally while user is typing.

@sparkboom
Copy link

sparkboom commented Aug 25, 2019

I managed to achieve the same result as @rohanBagchi , I've been experimenting with the next v2.0.1-rc13 hooks. Instead of useField, I've created a custom hook that was more performant with the cost of a slightly different behaviour (the values are updated on blur, however, the error state isn't resolved on change, but on blur)

export function useFastField(props)  {
  const [field, meta] = useField(props);
  const [value, setValue] = useState(field.value);
  const { onBlur, onChange } = field;

  field.value = value;
  field.onChange = (e) => {
    if (e && e.currentTarget) {
      setValue(e.currentTarget.value);
    }
  };
  field.onBlur = (e) => {
    onChange(e);
    onBlur(e);
  };

  return [field, meta];
}

With this, you can spread the fields and properties just like you do in useField

const MyField = () => {
  const [field, meta] = useFastField(props);

  return (
    <input {...props} {...field} />
  );
};

@mblarsen
Copy link

I noticed that on every key stroke two values always change:

  • errors an array of unchanged values
  • getFieldMeta which is a function

I used Jakob Rask's trace code:

function useTraceUpdate(label, props) {
  const prev = React.useRef(props)
  React.useEffect(() => {
    const changedProps = Object.entries(props).reduce((ps, [k, v]) => {
      if (prev.current[k] !== v) {
        console.log('Changed: ' + k)
        ps[k] = [prev.current[k], v]
      }
      return ps
    }, {})
    if (Object.keys(changedProps).length > 0) {
      console.log(`Changed props in ${label}:`, changedProps)
    }
    prev.current = props
  })
}

@stale stale bot removed the stale label Jan 30, 2020
@ghost
Copy link

ghost commented Jan 30, 2020

@mblarsen I am reading Formik's code, it seems that getFieldMeta is recomputed each time errors changes, so it should be enough to avoid updating errors.
Looking at:
https:/jaredpalmer/formik/blob/master/packages/formik/src/Formik.tsx
I see that state.errors could be recomputed in the main reducer for the case 'SET_FIELD_ERROR'.

case 'SET_FIELD_ERROR': return { ...state, errors: setIn(state.errors, msg.payload.field, msg.payload.value), };

Could it be that setIn isn't correctly returning the original state.errors if no field was updated?

@mblarsen
Copy link

Could it be that setIn isn't correctly returning the original state.errors if no field was updated?

That would surely explain the inequality of the errors, despite being unchanged.

@chiechelski
Copy link

Same issue for my form with over 1K fields.
The method OnChange seems to be the problem, so I only trigger the formik onChange onBlur like Rohan's suggestion.

const SimpleField = (props: SimpleFieldProps) => {
  const { index, field, formik } = props;
  const value = formik.values["clients"][index][field];

  const [fieldValue, setFieldValue] = useState(value);

  return useMemo(() => {
    return (
        <input
          type="text"
          className={"MuiInputFast-input"}
          id={`clients.${index}["${field}"]`}
          name={`clients.${index}["${field}"]`}
          onChange={(e: any) => setFieldValue(e.target.value)}
          onBlur={(e: any) => {
            formik.handleBlur(e);
            formik.handleChange(e);
          }}
          value={fieldValue}
        />
    )
  }, [field, index, fieldValue]);
}

@stale stale bot removed the stale label May 2, 2020
@santinozaracho
Copy link

Same issue here!, I just have only five fields, if remove handleBlur trigger, the performance change is several.

@mblarsen
Copy link

mblarsen commented Sep 9, 2020

Try https://react-hook-form.com/ easier to use and no more lag :)

@santinozaracho
Copy link

Try https://react-hook-form.com/ easier to use and no more lag :)

Thanks!!! A lot!!

@mattsputnikdigital
Copy link

mattsputnikdigital commented Dec 4, 2020

A few things I have noticed that cause a slow form, and how to fix them...

  1. If you are using react-create-app you will probably have <React.StrictMode> wrapping your app. This only functions in dev environments, and it intentionally causes a double render so it can internally debug your code. You can remove this if you wish as it is not necessary.

  2. If you have select fields, they will render the whole list on every update, keypress or whatever. You will need to cache / memorise values / components with long lists.

  3. Production builds do work much faster without React's internal development logging, debugging etc...

  4. You could separate Formik field props from the input / select components and memorise the input / select. This works for MaterialUI components and probably others as well.

const InputPrimitive: React.FunctionComponent<InputPrimitiveProps> = (props) => {
  //your expensive code goes here
  return (
    <div>
      <input {...props} />
    </div>
  );
};

const MemorisedInputPrimitive = React.memo(InputPrimitive);

const Input: React.FunctionComponent<InputProps> = (props) => {
  const [field] = useField(props.name);
  return <MemorisedInputPrimitive {...props} {...field} />;
};

The above stops all inputs but the one you are typing into re-rendering. Formik still performs all its calculations on every keypress, change, blur etc... but only the component being manipulated is rendered. Other on the page do not.

@jaredpalmer
Copy link
Owner

@mattsputnikdigital is spot on with the above solutions.

In general, any CSS-in-JS library that does runtime style calculations are slow. To add insult to injury, many CSS-in-JS solutions require you to use a <ThemeProvider> from which all components rely on--or a <Box> component primitive. The problem is that there is a ~2x performance penalty during render for each component that hooks into context. Multiply this by hundreds of components and it's death by 1000 cuts.

For a more detailed analysis of the issue, see this article: https://calendar.perfplanet.com/2019/the-unseen-performance-costs-of-css-in-js-in-react-apps/

So what can be done:

  • Use React.memo or shouldComponentUpdate to block renders
  • Use <Formik><Form>...</Form></Formik> to avoid the render prop inline function running on each keystroke
  • Consider switching to a zero-runtime CSS-in-JS solution
  • Avoid inline .map() of large lists
  • Upgrade to Formik 3 alpha which has a massively improved rendering strategy when using useField and FastField. (See the
    next branch)

@ChristianBermas
Copy link

You can try in formik SetFieldValue('fieldname', value, false) you can set shouldvalidate to false

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

No branches or pull requests