web-dev-qa-db-fra.com

Pénalité de performance de la création de gestionnaires sur chaque rendu avec des hooks réactifs

Je suis actuellement très étonné des cas d'utilisation de la nouvelle API react hooks et de ce que vous pouvez éventuellement en faire.

Une question qui s'est posée lors de l'expérimentation était de savoir combien il est coûteux de toujours créer une nouvelle fonction de gestionnaire juste pour la jeter lors de l'utilisation de useCallback.

Considérant cet exemple:

const MyCounter = ({initial}) => {
    const [count, setCount] = useState(initial);

    const increase = useCallback(() => setCount(count => count + 1), [setCount]);
    const decrease = useCallback(() => setCount(count => count > 0 ? count - 1 : 0), [setCount]);

    return (
        <div className="counter">
            <p>The count is {count}.</p>
            <button onClick={decrease} disabled={count === 0}> - </button>
            <button onClick={increase}> + </button>
        </div>
    );
};

Bien que j'encapsule le gestionnaire dans un useCallback pour éviter de transmettre un nouveau gestionnaire à chaque fois qu'il rend la fonction de flèche en ligne doit encore être créée uniquement pour être jeté dans la majorité des cas.

Probablement pas un gros problème si je ne rend que quelques composants. Mais quel est l'impact sur les performances si je le fais des milliers de fois? Y a-t-il une pénalité de performance notable? Et quel serait un moyen de l'éviter? Probablement une fabrique de gestionnaires statiques qui n'est appelée que lorsqu'un nouveau gestionnaire doit être créé?

16
trixn

Les React FAQs lui fournissent une explication

Les crochets sont-ils lents à cause de la création de fonctions dans le rendu?

Non. Dans les navigateurs modernes, les performances brutes des fermetures par rapport aux classes ne diffèrent pas de manière significative, sauf dans des scénarios extrêmes.

En outre, considérez que la conception des crochets est plus efficace de deux manières:

Les hooks évitent une grande partie des frais généraux requis par les classes, comme le coût de création d'instances de classe et de liaison des gestionnaires d'événements dans le constructeur.

Le code idiomatique utilisant Hooks n'a pas besoin de l'imbrication profonde des composants qui prévaut dans les bases de code qui utilisent des composants d'ordre supérieur, des accessoires de rendu et du contexte. Avec des arborescences de composants plus petites, React a moins de travail à faire.

Traditionnellement, les problèmes de performances concernant les fonctions en ligne dans React ont été liés à la façon dont le passage de nouveaux rappels à chaque rendu rend les optimisationsComponentUpdate dans les composants enfants. Les hooks abordent ce problème de trois côtés.

Les avantages globaux des crochets sont donc bien plus importants que la pénalité liée à la création de nouvelles fonctions

De plus, pour les composants fonctionnels, vous pouvez optimiser en utilisant useMemo pour que les composants soient restitués quand il n'y a pas de changement dans leurs accessoires.

6
Shubham Khatri

Mais quelle est l'ampleur de l'impact sur les performances si je le fais des milliers de fois? Y a-t-il une pénalité de performance notable?

Cela dépend de l'application. Si vous affichez simplement 1000 lignes de compteurs, c'est probablement correct, comme le montre l'extrait de code ci-dessous. Notez que si vous modifiez simplement l'état d'un individu <Counter />, seul ce compteur est restitué, les 999 autres compteurs ne sont pas affectés.

Mais je pense que vous êtes préoccupé par des choses non pertinentes ici. Dans les applications du monde réel, il est peu probable que 1000 éléments de liste soient rendus. Si votre application doit afficher 1000 éléments, il y a probablement un problème avec la façon dont vous avez conçu votre application.

  1. Vous ne devez pas afficher 1000 éléments dans le DOM. C'est généralement mauvais du point de vue des performances et de l'expérience utilisateur, avec ou sans cadres JavaScript modernes. Vous pouvez utiliser des techniques de fenêtrage et rendre uniquement les éléments que vous voyez à l'écran, les autres éléments hors écran peuvent être en mémoire.

  2. Implémentez shouldComponentUpdate (ou useMemo) afin que les autres éléments ne soient pas restitués si un composant de niveau supérieur doit effectuer un nouveau rendu.

  3. En utilisant des fonctions, vous évitez les frais généraux des classes et d'autres choses liées aux classes qui se passent sous le capot que vous ne connaissez pas parce que React le fait automatiquement pour vous. Vous en perdez performances en raison de l'appel de certains hooks dans les fonctions, mais vous gagnez également des performances ailleurs.

  4. Enfin, notez que vous appelez les hooks useXXX et n'exécutez pas les fonctions de rappel que vous avez passées dans les hooks. Je suis sûr que l'équipe React a fait du bon travail en rendant les hooks d'appel légers d'appel de hooks ne devrait pas être trop cher.

Et quel serait un moyen de l'éviter?

Je doute qu'il y ait un scénario réel où vous devrez créer des éléments avec état mille fois. Mais si vous le devez vraiment, il serait préférable de soulever l'état dans un composant parent et de passer la valeur et le rappel d'incrémentation/décrémentation comme accessoire dans chaque élément. De cette façon, vos éléments individuels n'ont pas à créer de rappels de modificateur d'état et peuvent simplement utiliser l'accessoire de rappel de son parent. En outre, les composants enfants sans état facilitent la mise en œuvre des diverses optimisations de performances bien connues.

Enfin, je tiens à réitérer que vous ne devriez pas vous inquiéter de ce problème car vous devriez essayer d'éviter de vous retrouver dans une telle situation au lieu de la gérer, en tirant parti de techniques telles que le fenêtrage et la pagination - en ne chargeant que les données que vous besoin d'afficher sur la page actuelle.

const Counter = ({ initial }) => {
  const [count, setCount] = React.useState(initial);

  const increase = React.useCallback(() => setCount(count => count + 1), [setCount]);
  const decrease = React.useCallback(
    () => setCount(count => (count > 0 ? count - 1 : 0)),
    [setCount]
  );

  return (
    <div className="counter">
      <p>The count is {count}.</p>
      <button onClick={decrease} disabled={count === 0}>
        -
      </button>
      <button onClick={increase}>+</button>
    </div>
  );
};

function App() {
  const [count, setCount] = React.useState(1000);
  return (
    <div>
      <h1>Counters: {count}</h1>
      <button onClick={() => {
        setCount(count + 1);
      }}>Add Counter</button>
      <hr/>
      {(() => {
        const items = [];
        for (let i = 0; i < count; i++) {
          items.Push(<Counter key={i} initial={i} />);
        }
        return items;
      })()}
    </div>
  );
}


ReactDOM.render(
  <div>
    <App />
  </div>,
  document.querySelector("#app")
);
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id="app"></div>
2
Yangshun Tay

Vous avez raison, dans les grandes applications, cela peut entraîner des problèmes de performances. La liaison du gestionnaire avant de le transmettre au composant évite que le composant enfant ne fasse un nouveau rendu supplémentaire.

<button onClick={(e) => this.handleClick(e)}>click me!</button>
<button onClick={this.handleClick.bind(this)}>click me!</button>

Les deux sont équivalents. L'argument e représentant l'événement React, alors qu'avec une fonction flèche, nous devons le passer explicitement, avec bind tous les arguments sont automatiquement transmis.

0
Fabian Hinsenkamp