Lutando pelo desempenho de formulários React verdadeiramente grandes

Em um dos projetos, encontramos formulários de várias dezenas de blocos que dependem uns dos outros. Como de costume, não podemos falar sobre a tarefa em detalhes por causa do NDA, mas tentaremos descrever nossa experiência de “domesticar” o desempenho dessas formas usando um exemplo abstrato (mesmo que ligeiramente não-vida). Vou lhe dizer quais conclusões tiramos de um projeto React com o formato final.



imagem


Imagine que o formulário permite que você obtenha um passaporte estrangeiro de uma nova amostra, enquanto processa o recebimento de um visto Schengen por meio de um intermediário - um centro de vistos. Este exemplo parece burocrático o suficiente para demonstrar nossas complexidades.



Então, em nosso projeto, nos deparamos com uma forma de muitos blocos com certas propriedades:



  • Entre os campos, há caixas de entrada, campos de múltipla escolha e preenchimento automático.
  • Os blocos estão interligados. Suponha que em um bloco você precise especificar os dados do passaporte interno, e logo abaixo haverá um bloco com os dados do solicitante do visto. Ao mesmo tempo, um acordo com um centro de vistos também é emitido para um passaporte interno.

  • – , , ( 10 , ) .
  • , , . , 10- , . : .
  • . . .


A forma final ocupava cerca de 6 mil pixels verticalmente - isso é cerca de 3-4 telas, no total, mais de 80 campos diferentes. Em comparação com este formulário, as aplicações nos Serviços do Estado não parecem tão boas. O mais próximo em termos de abundância de perguntas é provavelmente um questionário do serviço de segurança para alguma grande empresa ou uma enfadonha pesquisa de opinião sobre as preferências de conteúdo de vídeo.



Formulários grandes não são tão comuns em problemas reais. Se tentarmos implementar tal formulário “de frente” - por analogia com a forma como estamos acostumados a trabalhar com pequenos formulários - então o resultado será impossível de usar.



O principal problema é que à medida que você insere cada letra nos campos apropriados, todo o formulário é redesenhado, o que acarreta problemas de desempenho, principalmente em dispositivos móveis.



E é difícil lidar com o formulário não apenas para os usuários finais, mas também para os desenvolvedores que precisam mantê-lo. Se você não seguir etapas especiais, o relacionamento entre os campos no código será difícil de rastrear - as mudanças em um local implicam em consequências que às vezes são difíceis de prever.



Como implantamos o formulário final



O projeto usou React e TypeScript (conforme concluímos nossas tarefas, mudamos completamente para TypeScript). Portanto, para implementar os formulários, pegamos a biblioteca React Final-form dos criadores do Redux Form.



No início do projeto, dividimos o formulário em blocos separados e usamos as abordagens descritas na documentação do formulário final. Infelizmente, isso levou ao fato de que a entrada em um dos campos alterou todo o formulário grande. Como a biblioteca é relativamente recente, a documentação lá ainda é jovem. Não descreve as melhores receitas para melhorar o desempenho de grandes moldes. Pelo que entendi, muito poucas pessoas se deparam com isso em projetos. E para formulários pequenos, alguns redesenhos extras do componente não afetam o desempenho.



Dependências



A primeira obscuridade que tivemos que enfrentar foi como exatamente implementar a dependência entre os campos. Se você trabalhar estritamente de acordo com a documentação, o formulário overgrown começa a ficar lento devido ao grande número de campos interligados. A questão são as dependências. A documentação sugere colocar uma assinatura em um campo externo próximo ao campo. Foi assim no nosso projeto - versões adaptadas de react-final-form-listeners, que eram responsáveis ​​por vincular os campos, ficavam no mesmo lugar que os componentes, ou seja, ficavam em cada esquina. Dependências eram difíceis de rastrear. Isso aumentou a quantidade de código - os componentes eram gigantescos. E tudo funcionou devagar. E para mudar algo no formulário, era necessário gastar muito tempo usando a busca em todos os arquivos do projeto (existem cerca de 600 arquivos no projeto, mais de 100 deles são componentes).



Fizemos várias tentativas para melhorar a situação.



Tivemos que implementar nosso próprio seletor, que seleciona apenas os dados necessários para um determinado bloco.



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


Como você pode imaginar, tive que criar o meu próprio memoize pick([field1, field2,...fieldn]).



Tudo isso, em conjunto com, PureComponent (React.memo, reselect)levou ao fato de os blocos serem redesenhados apenas quando os dados dos quais eles dependem mudam (sim, introduzimos a biblioteca Reselect no projeto, que não era usada anteriormente, com a sua ajuda realizamos quase todas as solicitações de dados).



Como resultado, mudamos para um ouvinte, que descreve todas as dependências do formulário. Tiramos a própria ideia dessa abordagem do projeto final-form-calcule ( https://github.com/final-form/final-form-calculate ), adicionando-a às nossas necessidades.



<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,
         };
      },
   },
];


Como resultado, obtivemos a dependência necessária entre os campos. Além disso, os dados são armazenados em um só lugar e usados ​​de forma mais transparente. Além disso, sabemos em que ordem as assinaturas são acionadas, pois isso também é importante.



Validação



Por analogia com dependências, lidamos com validação.



Em quase todos os campos, é necessário verificar se a pessoa inseriu a idade correta (por exemplo, se o conjunto de documentos corresponde à idade especificada). De dezenas de validadores diferentes espalhados por todas as formas, mudamos para um global, dividindo-o em blocos separados:



  • validador de dados de passaporte,
  • validador de dados de viagem,
  • para dados sobre vistos anteriores emitidos,
  • etc.


Isso quase não afetou o desempenho, mas acelerou o desenvolvimento posterior. Agora, ao fazer alterações, você não precisa percorrer todo o arquivo para entender o que está acontecendo nos validadores individuais.



Reutilização de código



Começamos com um grande formulário, no qual rolamos nossas ideias, mas com o tempo, o projeto cresceu - outro formulário apareceu. Naturalmente, no segundo formulário, usamos todas as mesmas ideias e até reutilizamos o código.



Anteriormente, já movíamos toda a lógica para módulos separados, então por que não conectá-los ao novo formulário? Dessa forma, reduzimos significativamente a quantidade de código e a velocidade de desenvolvimento.



Da mesma forma, o novo formulário agora tem tipos, constantes e componentes comuns com o antigo - por exemplo, eles têm autorização geral.



Em vez de totais



A pergunta é lógica: por que não usamos outra biblioteca para formulários, já que esta tinha dificuldades. Mas os grandes formulários terão seus próprios problemas de qualquer maneira. No passado, eu mesmo trabalhei com Formik. Levando em consideração que encontramos soluções para nossas dúvidas, o formulário final revelou-se mais conveniente.



No geral, essa é uma ótima ferramenta para trabalhar com formulários. E junto com algumas regras para o desenvolvimento da base de código, ele nos ajudou a otimizar significativamente o desenvolvimento. Um bônus adicional a todo esse trabalho é a capacidade de atualizar os novos membros da equipe com mais rapidez.



Depois de destacar a lógica, ficou muito mais claro do que depende um determinado campo - não é necessário ler três planilhas de requisitos em analítica para isso. Nessas condições, a auditoria de bugs agora leva pelo menos duas horas, embora possa levar alguns dias antes de todas essas melhorias. Todo esse tempo, o desenvolvedor estava procurando um erro fantasma, que não fica claro pelo que ele se manifesta.



Autores do artigo: Oleg Troshagin, Maxilekt.



PS Publicamos nossos artigos em vários sites do Runet. Assine nossas páginas no canal VK , FB , Instagram ou Telegram para conhecer todas as nossas publicações e demais novidades da Maxilect.



All Articles