Lutte pour la performance des formulaires React vraiment volumineux

Sur l'un des projets, nous sommes tombés sur des formulaires provenant de plusieurs dizaines de blocs qui dépendent les uns des autres. Comme d'habitude, nous ne pouvons pas parler de la tâche en détail à cause de la NDA, mais nous essaierons de décrire notre expérience de «dompter» la performance de ces formes à l'aide d'un exemple abstrait (même légèrement non-vie). Je vais vous dire quelles conclusions nous avons tirées d'un projet React avec Final-form.



image


Imaginez que le formulaire vous permette d'obtenir un passeport étranger d'un nouvel échantillon, tout en traitant la réception d'un visa Schengen par un intermédiaire - un centre de visa. Cet exemple semble suffisamment bureaucratique pour démontrer nos complexités.



Ainsi, sur notre projet, nous sommes confrontés à une forme de nombreux blocs avec certaines propriétés:



  • Parmi les champs, il y a des zones de saisie, des champs à choix multiples et des champs à saisie automatique.
  • Les blocs sont liés entre eux. Supposons que, dans un bloc, vous devez spécifier les données du passeport interne, et juste en dessous, il y aura un bloc avec les données du demandeur de visa. Dans le même temps, un accord avec un centre de visa est également délivré pour un passeport interne.

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


La forme finale occupait environ 6 000 pixels verticalement - soit environ 3-4 écrans, au total, plus de 80 champs différents. En comparaison avec ce formulaire, la candidature aux services de l'État ne semble pas si grande. La chose la plus proche en termes d'abondance de questions est probablement un questionnaire du service de sécurité à une grande entreprise ou un sondage d'opinion ennuyeux sur les préférences du contenu vidéo.



Dans les problèmes réels, les grandes formes ne sont pas si courantes. Si nous essayons d'implémenter un tel formulaire «de front» - par analogie avec la façon dont nous sommes habitués à travailler avec de petits formulaires - alors le résultat sera impossible à utiliser.



Le principal problème est que lorsque vous entrez chaque lettre dans les champs appropriés, le formulaire entier sera redessiné, ce qui entraîne des problèmes de performances, en particulier sur les appareils mobiles.



Et il est difficile de gérer le formulaire non seulement pour les utilisateurs finaux, mais aussi pour les développeurs qui doivent le maintenir. Si vous ne prenez pas de mesures spéciales, la relation des champs dans le code est difficile à suivre - les changements à un seul endroit entraînent des conséquences parfois difficiles à prévoir.



Comment nous avons déployé Final-form



Le projet a utilisé React et TypeScript (à la fin de nos tâches, nous sommes complètement passés à TypeScript). Par conséquent, pour implémenter les formulaires, nous avons pris la bibliothèque React Final-form des créateurs de Redux Form.



Au début du projet, nous avons divisé le formulaire en blocs séparés et avons utilisé les approches décrites dans la documentation de Final-form. Hélas, cela a conduit au fait que l'entrée dans l'un des champs a jeté un changement dans l'ensemble du grand formulaire. La bibliothèque étant relativement récente, la documentation y est encore jeune. Il ne décrit pas les meilleures recettes pour améliorer les performances des grands moules. Si je comprends bien, très peu de gens sont confrontés à cela sur des projets. Et pour les petits formulaires, quelques redessins supplémentaires du composant n'ont aucun effet sur les performances.



Dépendances



La première obscurité à laquelle nous avons dû faire face était de savoir comment implémenter exactement la dépendance entre les champs. Si vous travaillez strictement selon la documentation, le formulaire envahi commence à ralentir en raison du grand nombre de champs interconnectés. Le point est les dépendances. La documentation suggère de mettre un abonnement à un champ externe à côté du champ. C'est ainsi que cela s'est passé sur notre projet - des versions adaptées des écouteurs react-final-form, chargés de relier les champs, se trouvaient au même endroit que les composants, c'est-à-dire qu'ils se trouvaient dans tous les coins. Les dépendances étaient difficiles à retracer. Cela gonflait la quantité de code - les composants étaient gigantesques. Et tout a fonctionné lentement. Et pour changer quelque chose dans le formulaire, il fallait passer beaucoup de temps à utiliser la recherche dans tous les fichiers du projet (il y a environ 600 fichiers dans le projet, plus de 100 d'entre eux sont des composants).



Nous avons fait plusieurs tentatives pour améliorer la situation.



Nous avons dû implémenter notre propre sélecteur, qui sélectionne uniquement les données nécessaires à un bloc particulier.



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


Comme vous pouvez l'imaginer, j'ai dû créer le mien memoize pick([field1, field2,...fieldn]).



Tout cela, en conjonction avec, a PureComponent (React.memo, reselect)conduit au fait que les blocs ne sont redessinés que lorsque les données dont ils dépendent changent (oui, nous avons introduit la bibliothèque Reselect dans le projet, qui n'était pas utilisée auparavant, avec son aide, nous effectuons presque toutes les demandes de données).



En conséquence, nous sommes passés à un seul écouteur, qui décrit toutes les dépendances du formulaire. Nous avons repris l'idée même de cette approche du projet final-form-Calculate ( https://github.com/final-form/final-form-calculate ), en l'ajoutant à nos besoins.



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


En conséquence, nous avons obtenu la relation requise entre les champs. De plus, les données sont stockées au même endroit et sont utilisées de manière plus transparente. De plus, nous savons dans quel ordre les abonnements sont déclenchés, car cela est également important.



Validation



Par analogie avec les dépendances, nous avons traité de la validation.



Dans presque tous les champs, nous devions vérifier si la personne avait entré l'âge correct (par exemple, si l'ensemble de documents correspond à l'âge spécifié). À partir de dizaines de validateurs différents répartis sur tous les formulaires, nous sommes passés à un seul global, en le décomposant en blocs séparés:



  • validateur pour les données de passeport,
  • validateur pour les données de voyage,
  • pour les données sur les visas précédemment délivrés,
  • etc.


Cela n'a presque pas affecté les performances, mais a accéléré le développement ultérieur. Désormais, lorsque vous apportez des modifications, vous n'avez pas besoin de parcourir tout le fichier pour comprendre ce qui se passe dans les validateurs individuels.



Réutilisation du code



Nous avons commencé avec une grande forme, sur laquelle nous avons roulé nos idées, mais au fil du temps, le projet a grandi - une autre forme est apparue. Naturellement, sur le deuxième formulaire, nous avons utilisé toutes les mêmes idées, et même réutilisé le code.



Auparavant, nous avons déjà déplacé toute la logique dans des modules séparés, alors pourquoi ne pas les connecter au nouveau formulaire? De cette façon, nous avons considérablement réduit la quantité de code et la vitesse de développement.



De même, le nouveau formulaire a maintenant des types, des constantes et des composants communs avec l'ancien - par exemple, ils ont une autorisation générale.



Au lieu de totaux



La question est logique: pourquoi n’avons-nous pas utilisé une autre bibliothèque pour les formulaires, puisque celle-ci avait des difficultés. Mais les grandes formes auront de toute façon leurs propres problèmes. Dans le passé, j'ai moi-même travaillé avec Formik. Compte tenu du fait que nous avons trouvé des solutions à nos questions, le formulaire final s'est avéré plus pratique.



Dans l'ensemble, c'est un excellent outil pour travailler avec des formulaires. Et avec quelques règles pour le développement de la base de code, il nous a aidés à optimiser considérablement le développement. Le bonus supplémentaire de tout ce travail est la possibilité de mettre les nouveaux membres de l'équipe à jour plus rapidement.



Après avoir mis en évidence la logique, il est devenu beaucoup plus clair de quoi dépend un champ particulier - il n'est pas nécessaire de lire trois feuilles d'exigences dans l'analyse pour cela. Dans ces conditions, l'audit des bogues prend désormais au moins deux heures, bien que cela puisse prendre quelques jours avant toutes ces améliorations. Pendant tout ce temps, le développeur recherchait une erreur fantôme, qui n'est pas claire d'après ce qu'elle se manifeste.



Auteurs de l'article: Oleg Troshagin, Maxilekt.



PS Nous publions nos articles sur plusieurs sites sur le Runet. Abonnez-vous à nos pages sur la chaîne VK , FB , Instagram ou Telegram pour connaître toutes nos publications et autres actualités de Maxilect.



All Articles