Top.Mail.Ru

Борьба за производительность по-настоящему больших форм на React

На одном из проектов мы столкнулись с формами из нескольких десятков блоков, которые зависят друг от друга. Как обычно, мы не можем рассказать о задаче в деталях из-за NDA, но попробуем описать свой опыт “укрощения” производительности этих форм на абстрактном (даже немного не жизненном) примере. Расскажу, какие выводы мы сделали из проекта на React с Final-form.

Представьте, что форма позволяет вам получить заграничный паспорт нового образца, одновременно оформляя получение Шенгенской визы через посредника – визовый центр. Кажется, этот пример достаточно бюрократичен, чтобы продемонстрировать наши сложности.

Итак, на нашем проекте мы столкнулись с формой из множества блоков, обладающих определенными свойствами:

  • Среди полей есть окна для ввода, множественный выбор, поля с автозаполнением.
  • Блоки связаны между собой. Предположим, в одном блоке вам надо указать данные внутреннего паспорта, а чуть ниже будет блок с данными заявителя на визу. Договор с визовым центром при этом тоже оформляется на внутренний паспорт.
  • В каждом блоке надо реализовать свои проверки – на адекватность номера паспорта, корректность ввода электронной почты, возраст человека (в 10 лет загранпаспорт получить можно, а заказчиком по договору быть нельзя) и многое другое.
  • От данных, введенных в одни блоки, может зависеть видимость и автоматически введенные данные в других блоках. В примере выше, если загранпаспорт оформляется на 10-летнего школьника, нужно отобразить блок с данными родителей. Зависимости не тривиальны: одно поле может зависеть от пяти и более других полей.
  • Заполнение формы разделено на два шага. В первом шаге мы показываем лишь малую часть полей. Но введенную информацию мы должны помнить во втором шаге.

Итоговая форма занимала порядка 6 тыс. пикселей по вертикали – это примерно 3-4 экрана, в общей сложности более 80 разных полей. В сравнении с этой формой заявления на Госуслугах кажутся не такими уж и большими. Ближе всего по обилию вопросов, наверное, анкета службы безопасности в какую-нибудь большую корпорацию или скучный социологический опрос о предпочтениях видеоконтента.

В реальных задачах большие формы встречаются не так часто. Если пытаться реализовать такую форму “в лоб” – по аналогии с тем, как мы привыкли работать с небольшими формами, – то результатом будет невозможно пользоваться.

Основная проблема заключается в том, что при вводе каждой буквы в соответствующих полях вся форма будет перерисовываться, что влечет за собой проблемы с производительностью, особенно на мобильных устройствах.

И тяжело совладать с формой не только конечным пользователям, но и разработчикам, которым предстоит ее поддерживать. Если не предпринимать специальных шагов, связь полей в коде сложно отследить – изменения в одном месте влекут за собой последствия, которые порой сложно прогнозировать.

Как мы развернули под себя Final-form

На проекте использовались React и TypeScript (по мере реализации своих задач мы полностью перешли на TypeScript). Поэтому для реализации форм мы взяли библиотеку React Final-form от создателей Redux Form.

На старте проекта мы разбили форму на отдельные блоки и использовали подходы, описанные в документации к Final-form. Увы, это приводило к тому, что ввод в одном из полей кидал изменение всей большой формы. Поскольку библиотека сравнительно свежая, документация там также пока “молода”. В ней не описаны оптимальные рецепты, позволяющие улучшить производительность больших форм. Как я понимаю, с этим просто мало кто сталкивается на проектах. А для маленьких форм несколько лишних перерисовок компонента не оказывают никакого влияния на производительность.

Зависимости

Первая неясность, с которой нам пришлось столкнуться, – как именно реализовать зависимость между полями. Если работать строго по документации, разросшаяся форма начинает тормозить из-за большого количества связанных между собой полей. Суть тут в зависимостях. Документация предлагает положить рядом с полем подписку на внешнее поле. У нас на проекте так и было – адаптированные варианты react-final-form-listeners, которые отвечали за связь полей, лежали там же, где и компоненты, то есть валялись в каждом углу. Уследить за зависимостями было трудно. Это раздуло объем кода – компоненты стали просто гигантскими. Да и работало все медленно. А чтобы что-то изменить в форме, приходилось тратить много времени, используя поиск по всем файлам проекта (в проекте порядка 600 файлов, из них более 100 – компоненты).

Мы сделали несколько попыток улучшить ситуацию.

Нам пришлось реализовать собственный selector, который выбирает только данные, необходимые какому-то конкретному блоку.

<Form onSubmit={this.handleSubmit} initialValues={initialValues}>
   {({values, error, ...other}) => (
      <>
      <Block1 data={selectDataForBlock1(values)}/>
      <Block2 data={selectDataForBlock2(values)}/>
      ...
      <BlockN data={selectDataForBlockN(values)}/>
      </>
   )}
</Form>

Как вы понимаете, пришлось придумывать свой memoize pick([field1, field2,...fieldn]).

Все это в связке с PureComponent (React.memo, reselect) привело к тому что блоки перерисовываются только тогда, когда меняются данные, от которых они зависят (да, мы ввели в проект библиотеку Reselect, которая ранее не использовалась, с ее помощью выполняем почти все запросы данных).

В итоге перешли на один listener, где описаны все зависимости для формы. Саму идею этого подхода мы взяли из проекта final-form-calculate (https://github.com/final-form/final-form-calculate), допилив под свои нужды.

<Form
   onSubmit={this.handleSubmit}
   initialValues={initialValues}
   decorators={[withContextListenerDecorator]}
>

   export const listenerDecorator = (context: IContext) =>
   createDecorator(
      ...block1FieldListeners(context),
      ...block2FieldListeners(context),
      ...
   );

   export const block1FieldListeners = (context: any): IListener[] => [
      {
      field: 'block1Field',
      updates: (value: string, name: string) => {
         // Когда изменеяется поле block1Field срабатывает эта функция и мы зависимые поля...
         return {
            block2Field1: block2Field1NewValue,
            block2Field2: block2Field2NewValue,
         };
      },
   },
];

В итоге мы получили искомую зависимость между полями. Плюс данные хранятся в одном месте и более прозрачно используются. Более того, мы знаем, в каком порядке срабатывают подписки, поскольку это тоже важно.

Валидация

По аналогии с зависимостями мы разобрались и с валидацией.

Ввод почти в каждое поле нам необходимо было проверять – ввел ли человек правильный возраст (соответствует ли, например, набор документов указанному возрасту). С десятков разных валидаторов, разбросанных по все форме, мы перешли на один глобальный, разбив его на отдельные блоки:

  • валидатор для паспортных данных,
  • валидатор для данных о поездке,
  • для данных о предыдущих выданных визах,
  • и т.п.

Это почти не сказалось на производительности, зато ускорило дальнейшую разработку. Теперь при внесении изменений не нужно пробегать по всему файлу, чтобы понять, что происходит в отдельных валидаторах.

Переиспользование кода

Начинали мы с одной большой формы, на которой и обкатывали свои идеи, но со временем проект разросся – появилась еще одна форма. Естественно, на второй форме мы использовали все те же идеи, да еще и код переиспользовали.

Ранее всю логику мы уже вынесли в отдельные модули, так почему бы не подключить их к новой форме? Так мы существенно сократили количество кода и скорость разработки.

Аналогично у новой формы появились общие со старой типы, константы и компоненты – в них, например, попала общая авторизация.

Вместо итогов

Логичен вопрос: почему мы не использовали другую библиотеку для форм, раз уж с этой возникли сложности. Но с большими формами в любом случае будут свои проблемы. В прошлом я и сам работал с Formik. С учетом того, что решения для своих вопросов мы все-таки нашли, Final-form оказался удобнее.

В целом это отличный инструмент для работы с формами. А вместе с некоторыми правилами развития кодовой базы он помог нам значительно оптимизировать разработку. Дополнительный бонус всей этой работы – это возможность быстрее вводить в курсе дела новых членов команды.

После выделения логики стало гораздо понятнее, от чего зависит конкретное поле, – необязательно для этого читать три листа требований в аналитике. В этих условиях аудит багов теперь занимает от силы два часа, хотя до всех этих усовершенствований он мог отнять пару дней. Все это время разработчик выискивал фантомную ошибку, которая непонятно от чего проявляется.

Авторы статьи: Олег Трошагин, Максилект.

Наши статьи по теме:

Все статьи

Связаться с нами

Мы свяжемся с вами в течение 24 часов.