J'ai pris un exemple de TodoList pour refléter mon problème, mais mon code du monde réel est évidemment plus complexe.
J'ai un pseudo-code comme celui-ci.
var Todo = React.createClass({
mixins: [PureRenderMixin],
............
}
var TodosContainer = React.createClass({
mixins: [PureRenderMixin],
renderTodo: function(todo) {
return <Todo key={todo.id} todoData={todo} x={this.props.x} y={this.props.y} .../>;
},
render: function() {
var todos = this.props.todos.map(this.renderTodo)
return (
<ReactCSSTransitionGroup transitionName="transition-todo">
{todos}
</ReactCSSTransitionGroup>,
);
}
});
Toutes mes données sont immuables, et PureRenderMixin est utilisé correctement et tout fonctionne correctement. Lorsqu'une donnée Todo est modifiée, seuls le parent et la tâche modifiée sont à nouveau rendus.
Le problème est qu’à un moment donné, ma liste s’allonge au fur et à mesure que l’utilisateur défile. Et lorsqu'un seul Todo est mis à jour, le rendu du parent prend de plus en plus de temps, appelez shouldComponentUpdate
sur tous les todos, puis restituez le seul todo.
Comme vous pouvez le constater, le composant Todo a un autre composant que les données Todo. Ce sont des données qui sont requises pour le rendu par tous les todos et qui sont partagées (par exemple, nous pourrions imaginer un "displayMode" pour les todos). Ayant plusieurs propriétés, la shouldComponentUpdate
exécute un peu plus lentement.
De plus, utiliser ReactCSSTransitionGroup
semble également ralentir un peu, car ReactCSSTransitionGroup
doit se rendre lui-même et ReactCSSTransitionGroupChild
même avant que la shouldComponentUpdate
de todos soit appelée. React.addons.Perf
indique que le rendu de ReactCSSTransitionGroup > ReactCSSTransitionGroupChild
est une perte de temps pour chaque élément de la liste.
Donc, autant que je sache, j'utilise PureRenderMixin
, mais avec une liste plus longue, cela peut ne pas suffire. Je n’obtiens toujours pas de si mauvaises performances, mais je voudrais savoir s’il existe des moyens simples d’optimiser mes rendus.
Une idée?
Modifier:
Jusqu'à présent, ma grande liste est paginée. Par conséquent, au lieu d'avoir une grande liste d'éléments, je divise maintenant cette grande liste en une liste de pages. Cela permet d’obtenir de meilleures performances puisque chaque page peut maintenant implémenter shouldComponentUpdate
. Désormais, lorsqu'un élément change dans une page, React doit uniquement appeler la fonction de rendu principale qui itère sur la page, et uniquement à partir d'une seule page, ce qui simplifie considérablement le travail d'itération.
Cependant, mes performances de rendu sont toujours linéaires par rapport au numéro de page (O (n)) que j'ai. Donc, si j'ai des milliers de pages, c'est toujours le même problème :) Dans mon cas d'utilisation, il est peu probable que cela se produise, mais je suis toujours intéressé par une meilleure solution.
Je suis pratiquement sûr qu'il est possible d'obtenir des performances de rendu O(log(n)) où n est le nombre d'éléments (ou de pages), en scindant une grande liste en une arborescence (telle qu'une structure de données persistante), et où chaque noeud a le pouvoir de court-circuiter le calcul avec shouldComponentUpdate
Oui, je pense à quelque chose qui ressemble à des structures de données persistantes comme Vector dans Scala ou Clojure:
Cependant, je suis préoccupé par React car, autant que je sache, il peut être nécessaire de créer des noeuds dom intermédiaires lors du rendu des noeuds internes de l’arborescence. Cela peut être un problème selon le cas d'utilisation ( et peut être résolu dans les futures versions de React )
Aussi, comme nous utilisons Javascript, je me demande si Immutable-JS le supporte et rend les "nœuds internes" accessibles. Voir: https://github.com/facebook/immutable-js/issues/541
Edit: lien utile avec mes expériences: Une application React-Redux peut-elle vraiment évoluer aussi bien que, disons, Backbone? Même avec resélectionner. Sur le mobile
Voici une implémentation POC que j'ai faite avec la structure interne d'ImmutableJS. Ce n'est pas une API publique, donc elle n'est pas prête pour la production et ne gère pas actuellement les cas de coin, mais cela fonctionne.
var ImmutableListRenderer = React.createClass({
render: function() {
// Should not require to use wrapper <span> here but impossible for now
return (<span>
{this.props.list._root ? <GnRenderer gn={this.props.list._root}/> : undefined}
{this.props.list._tail ? <GnRenderer gn={this.props.list._tail}/> : undefined}
</span>);
}
})
// "Gn" is the equivalent of the "internal node" of the persistent data structure schema of the question
var GnRenderer = React.createClass({
shouldComponentUpdate: function(nextProps) {
console.debug("should update?",(nextProps.gn !== this.props.gn));
return (nextProps.gn !== this.props.gn);
},
propTypes: {
gn: React.PropTypes.object.isRequired,
},
render: function() {
// Should not require to use wrapper <span> here but impossible for now
return (
<span>
{this.props.gn.array.map(function(gnItem,index) {
// TODO should check for Gn instead, because list items can be objects too...
var isGn = typeof gnItem === "object"
if ( isGn ) {
return <GnRenderer gn={gnItem}/>
} else {
// TODO should be able to customize the item rendering from outside
return <span>{" -> " + gnItem}</span>
}
}.bind(this))}
</span>
);
}
})
Le code client ressemble à
React.render(
<ImmutableListRenderer list={ImmutableList}/>,
document.getElementById('container')
);
Voici un JsFiddle qui enregistre le nombre d'appels shouldComponentUpdate
après la mise à jour d'un seul élément de la liste (taille N): il n'est pas nécessaire d'appeler N fois shouldComponentUpdate
Des détails supplémentaires sur l'implémentation sont partagés dans ce numéro de github ImmutableJs
Dans notre produit, nous avions également des problèmes liés à la quantité de code restituée et nous avons commencé à utiliser des observables (voir this blog ). Cela pourrait partiellement résoudre votre problème, car la modification de todo n'aura plus besoin du composant parent qui contient la liste pour être restitué (mais l'ajout reste nécessaire).
Cela pourrait également vous aider à redonner plus rapidement le rendu de la liste, car vos composants todoItem pourraient simplement renvoyer false lorsque les accessoires changent sur shouldComponentUpdate.
Pour améliorer encore les performances lors du rendu de l'aperçu, je pense que votre idée d'arborescence/de pagination est vraiment intéressante. Avec des tableaux observables, chaque page pourrait commencer à écouter les épissures de tableau (à l'aide d'un ESB polyfill ou mobservable) dans une certaine plage. Cela introduirait une certaine administration, car ces plages pourraient changer avec le temps, mais devraient vous amener à O (log (n))
Donc, vous obtenez quelque chose comme:
var TodosContainer = React.createClass({
componentDidMount() {
this.props.todos.observe(function(change) {
if (change.type === 'splice' && change.index >= this.props.startRange && change.index < this.props.endRange)
this.forceUpdate();
});
},
renderTodo: function(todo) {
return <Todo key={todo.id} todoData={todo} x={this.props.x} y={this.props.y} .../>;
},
render: function() {
var todos = this.props.todos.slice(this.props.startRange, this.props.endRange).map(this.renderTodo)
return (
<ReactCSSTransitionGroup transitionName="transition-todo">
{todos}
</ReactCSSTransitionGroup>,
);
}
});
Le problème central des grandes listes et de la réaction semble être que vous ne pouvez pas simplement déplacer de nouveaux nœuds DOM dans le dom. Autrement, vous n'aurez plus besoin des 'pages' pour partitionner les données en petits morceaux et vous pouvez simplement épisser un nouvel élément Todo dans le dom, comme avec JQuery dans ce jsFiddle . Vous pouvez toujours le faire en réagissant si vous utilisez une ref
pour chaque tâche, mais cela fonctionnerait avec le système, ce qui risquerait de briser le système de rapprochement?
Récemment, j'ai eu un goulot d'étranglement en essayant de générer un tableau avec plus de 500 enregistrements, les réducteurs étaient immuables et j'utilisais resélection pour mémoriser les sélecteurs complexes.