class: center, middle, cover # React et la performance Web ## Quelques astuces pour éviter les problèmes --- # Timothée .flex[  * Timothée Pillard [@tpillard](https://twitter.com/tpillard) * à Gandi depuis ~4 ans * développe des trucs sur les sites Web de Gandi: site public , espace client, shop... ] --- # Julien .flex[  * Julien Wajsberg [@jwajsberg](https://twitter.com/jwajsberg) * à Mozilla depuis plus de 5 ans * développeur sur [perf.html](https://github.com/devtools-html/perf.html/) depuis plus d'un an ] --- # On va parler de... 1. Les outils 2. React 3. Redux 4. à retenir ! --- class: center, middle # Jetons un œil à cette application React --- # Quelques URLs * Accéder à l'application: https://parisweb.app/ * GitHub: https://github.com/ziir/pariswebapp --- # L'application * affichage des conférences Paris-Web depuis plusieurs années * possibilité de filtres et de tris * possibilité d'indiquer les conférences où l'on souhaite aller, avec persistance --- # Cette application a quelques problèmes * rendu coûteux * rendu complet régulièrement * rendu complet à chaque interaction -- * ... regardons de plus près ce qui se passe --- class: center, middle # Des outils pour nous aider --- # Mesurer avant de corriger Dans le monde de la perf, l'intuition est mauvaise conseillère. Généralement il faut donc *mesurer avant de corriger*. -- Et *après* avoir corrigé. --- # react-devtools Possibilité de "highlight" les parties de la page qui sont mises à jour. React 16.5 inclut une API pour que react-devtools puisse afficher des informations de profiling.  --- # redux-logger  * affiche l'action et *la durée* pour la traiter de bout en bout * affiche le `state` avant/après * permet aussi un *diff* * le state ne peut pas être GC: ne pas l'activer en production --- # redux-devtools .center[  ] --- # redux-devtools * à peu près pareil, mais dans une interface pratique, en extension * Attention à l'overhead pour des stores volumineux. On peut utiliser des options lors de l'initialisation pour limiter cela. --- # profilers   --- # profilers React 16, en mode développement, injecte des marqueurs indiquant les renders. --- # profilers Ils permettent aussi de voir dans quelle partie de votre code on passe le plus de temps. .center[   ] --- # profilers ## Chrome affiche des infos par ligne .center[  ] --- class: center, middle # Regardons ce qui se passe pour `react-1` --- class: center, middle # Regardons de plus près comment React fait son rendu --- # Comment React fait le rendu ## Premier rendu Il n'y a qu'une seule manière de déclencher un premier rendu: appeler `ReactDOM.render`: ```js ReactDOM.render(
, document.getElementById('root') ); ``` React appelle la méthode `render` de cet élément, puis continue récursivement avec tous les enfants. --- # Comment React fait le rendu ## Premier rendu .center[
graph TD Root --> Component Component --> Content
] --- # Comment React fait le rendu ## Updates suivantes Une update peut être déclenchée: 1. par un nouvel appel à `ReactDOM.render` avec le même composant 2. lorsque le `state` d'un composant change avec la méthode `setState`. .smallest[C'est aussi ainsi que les composants connectés à Redux sont mis à jour.] 3. lorsque les `props` d'un composant changent. Cela est forcément une conséquence des deux premiers cas. --- # Comment React fait le rendu ## Algorithme de réconciliation .flex[
graph TD Root --> Component Component --> NewContent style NewContent fill:#f88 style Root stroke-width:4px
.group[ Lorsqu'il y a une update d'un composant, React va appeler la méthode `render` de ce composant. Puis il compare cela avec le retour précédent. ] ] --- # Comment React fait le rendu ## Algorithme de réconciliation .flex[
graph TD Root --> Component Component --> NewContent style NewContent fill:#f88 style Component stroke-width:4px
Lorsque l'élément au même endroit est du même type, React ne va que changer ses props si besoin et réaliser une *update* de l'instance existante. ] --- # Comment React fait le rendu ## Algorithme de réconciliation .flex[
graph TD Root --> Component Component --> NewContent style NewContent fill:#f88 style NewContent stroke-width:4px
Sinon il va *unmount* l'instance existante et *mount* une nouvelle instance du nouveau composant. ] --- # Rappel: cycles de vie ## Mount .center[
graph TD componentWillMount[componentWillMount / getDerivedStateFromProps] construct --> componentWillMount componentWillMount --> render render --> componentDidMount
] --- # Rappel: cycles de vie ## Unmount .center[
graph TD componentWillUnmount
] --- # Rappel: cycles de vie ## Update .center[
graph TD componentWillReceiveProps[componentWillReceiveProps / getDerivedStateFromProps] componentWillReceiveProps --> shouldComponentUpdate shouldComponentUpdate --> componentWillUpdate componentWillUpdate --> render render --> getSnapshotBeforeUpdate getSnapshotBeforeUpdate --> componentDidUpdate
] --- # Algorithme de mise à jour: conséquences 1. L'algorithme est joué à chaque update ⇨ attention à ne pas réaliser d'updates plus souvent que nécessaire. 2. Les méthodes `render` ainsi que les autres méthodes du cycle de vie d'un rendu sont exécutées à chaque update ⇨ attention à leurs caractéristiques de performance. 3. L'algorithme lui-même est O(n) d'après les développeurs de React. Donc un arbre plus gros va linéairement augmenter la durée de l'algorithme. --- # 1. Ne pas réaliser plus d'updates que nécessaire Une update sera déclenchée à chaque appel à `setState`. Donc: * éviter de mettre dans le `state` des choses inutiles au `render`. * éviter de déclencher trop de `setState` en réponse à des event handlers. Solution possible: debounce --- # 2. Attention à la performance de `render` * traiter le rendu d'une liste: diviser en morceaux ou rendre la liste virtuelle. * attention aux calculs coûteux: il vaut mieux les faire en amont et stocker le résultat dans le state. * c'est la même chose pour `componentWillUpdate`, `componentDidUpdate` etc. * un `setState` dans `componentDidUpdate` va déclencher un nouveau cycle d'update --- # 3. Aider l'algorithme de réconciliation à vous aider * essayer de changer l'état au plus proche du changement d'UI. Redux peut aider ici. * `shouldComponentUpdate` et `PureComponent` sont des bons outils pour stopper la propagation de l'update, en quelque sorte pour "couper" des branches de l'arbre. * lorsqu'on rend des listes, il ne faut pas oublier de mettre l'attribut *key*. Cela permet à la réconciliation de retrouver et comparer des éléments **même lorsque l'ordre change**. --- class: center, middle # Maintenant qu'on a corrigé l'application, regardons la de plus près... --- # Reparlons de `shouldComponentUpdate` * appelée au début du processus d'update d'un composant * si la méthode retourne `false` le rendu est arrêté pour ce composant... et tous ses enfants * très bonne manière de diminuer le coût d'une passe de rendu --- # `shouldComponentUpdate` ## Un exemple .smallest[ ```js class ComplexPanel extends React.Component { // Note: this syntax, new but supported by Babel, automatically binds the // method with the object instance. onClick = () => { this.setState({ detailsOpen: true }); } // Return false to avoid a render * shouldComponentUpdate(nextProps, nextState) { // Note: this works only if `summary` and `content` are primitive data // (eg: string, number) or immutable data // (keep reading to know more about this) * return nextProps.summary !== this.props.summary * || nextProps.content !== this.props.content * || nextState.detailsOpen !== this.state.detailsOpen; } render() { return (
{this.state.detailsOpen ?
: null}
); } } ``` ] --- # `shouldComponentUpdate` ## Les pièges * Exécuté à chaque render cycle, donc il doit être performant. Donc pas de comparaison coûteuse. * Oublier de le modifier lorsqu'on rajoute des propriétés ou du state au composant. D'où l'intérêt de... --- # `PureComponent` ## Une implémentation par défaut Un `PureComponent` n'est qu'un `Component` avec une implémentation de `shouldComponentUpdate` efficace: elle vérifie si chaque prop et chaque propriété d'état est strictement égale à sa valeur précédente. Si oui, elle retourne `false` et le rendu est annulé. Cela ne fonctionne que si on applique l'*immutabilité* de ces valeurs. .smallest[plus d'infos dans quelques instants...] --- # `PureComponent` ## Le même exemple, en plus simple .smaller[ ```js *class ComplexPanel extends React.PureComponent { // Note: this syntax, new but supported by Babel, automatically binds the // method with the object instance. onClick = () => { // Running this repeatidly won't render more than once. this.setState({ detailsOpen: true }); } render() { return (
{this.state.detailsOpen ?
: null}
); } } ``` ] --- class: center, middle On appelle *immutabilité* le principe selon lequel on ne change plus le contenu d'une structure après sa création. --- class: center, middle Autrement dit, si on veut changer le contenu d'une structure, on doit *recréer cette structure*. -- .smaller[Tout en réutilisant ce qui ne change pas.] --- class: center, middle Un exemple ? --- # Immutabilité ## Un exemple avec un tableau ```js // Ajouter un élément dans un tableau stateArray = [...stateArray, newElement]; // Pour changer une propriété du state this.setState(({ stateArray }) => ({ stateArray: [...stateArray, newElement], })); ``` Notez qu'on crée un nouveau tableau, mais on ne recrée pas chacun des éléments car eux n'ont pas changé. --- # Immutabilité ## Exemple pour retirer un élément d'un tableau ```js this.setState(({ stateArray }) => { // Removing an element is more involved. const newArray = stateArray.filter( element => element !== removeElement ); return { stateArray: newArray }; }); ``` -- Que se passe-t-il lorsque l'élément n'est pas présent dans le tableau ? --- class: center, middle On doit recréer une structure si on veut changer le contenu de cette structure. --- class: center, middle On doit recréer une structure *si et seulement si* on veut changer le contenu de cette structure. --- class: center, middle Autrement dit: on ne change pas la structure si on ne change pas son contenu ! --- # Immutabilité ## Améliorons l'exemple ```js this.setState(({ stateArray }) => { // Removing an element is more involved. const newArray = stateArray.filter( element => element !== removeElement ); return { stateArray: * newArray.length === stateArray.length * ? stateArray * : newArray }; }); ``` --- # Trucs à faire attention ## Changer les références par inadvertance * `array.filter`, `array.map`, ... comme on l'a vu plus tôt * `[]`, `{}`: ```js return selectors.getUsefulData(state) || []; ``` * `function.bind` et fonctions anonymes: .smaller[ ```js render() { return
this.doSomething()} />; } ``` ] --- class: center, middle # Et si on parlait un peu de Redux ? --- # React avec Redux .smallest[ ```javascript connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])(Component); ``` ] Connecte un composant React au store Redux, en retournant une *nouvelle classe* de composant. --- # React avec Redux ```javascript connect(mapStateToProps)(Component); ``` * souscrit à chaque mise à jour du store Redux * appelée après chaque modification du `state` * il est _crucial _qu'elle soit performante --- # React avec Redux, rappels ```javascript connect(null, mapDispatchToProps)(Component); ``` `mapDispatchToProps` permet d'injecter des action creators sous forme de `props`. Meilleure syntaxe: ```js mapDispatchToProps = { doStuff: doStuffActionCreator }; ``` --- # React avec Redux ## Sélecteurs .smallest[ ```javascript // { // tabs: [{ // name: 'developers', // label: 'Developers, developers, developers' // }], // activeTab: 'developers', // } const selectTabs = (state) => state.tabs; ``` ] -- .smallest[ ```javascript const selectActiveTab = (state) => selectTabs(state).find((tab) => tab.name === state.activeTab); ``` ] -- .smallest[ ```javascript const selectActiveTranslatedTab = (state) => { const tab = selectActiveTab(state); return tab && { ...tab, translated_label: translate(tab.name) } }; ``` ] -- Autant que possible, retourner la même valeur en évitant de la recalculer à chaque fois. C'est ce qu'on appelle la _memoization_. --- # React avec Redux, en pratique ## 2. `createSelector` de `reselect` .smallest[ ```javascript // { // tabs: [{ // name: 'developers', // label: 'Developers, developers, developers' // }], // activeTab: 'developers', // } const selectTabs = (state) => state.tabs; ``` ] -- .smallest[ ```javascript const selectActiveTabName = (state) => state.activeTab; const selectActiveTab = createSelector( [selectTabs, selectActiveTabName], (tabs, name) => tabs.find((tab) => tab.name === name), ); ``` ] -- .smallest[ ```javascript const selectActiveTranslatedTab = createSelector( [selectActiveTab], (tab) => tab && { ...tab, translated_label: translate(tab.name) }, ); ``` ] --- # React avec Redux, en pratique ## .smaller[2.1 Fonctions `reducer`: une approche naive] ```javascript function reducer(state, action) { if (action.type === 'SET_ACTIVE_TAB') { return { tabs: state.tabs.map((tab) => ({ ...tab, active: action.activeTab === tab.name, })), }; } return state; } ``` --- # React avec Redux, en pratique ## .smaller[2.1 Fonctions `reducer`: une approche naive] .smaller[ ```javascript function reducer(state, action) { if ( action.type === 'SET_ACTIVE_TAB' && action.activeTab !== state.find((tab) => tab.active)?.name ) { return { tabs: state.tabs.map((tab) => ( tab.activeTab === (action.activeTab === tab.name) ? tab : { ...tab, active: action.activeTab === tab.name, } )), }; } return state; } ``` ] --- # React avec Redux, en pratique ## .smaller[2.2 Fonctions `reducer`: une approche plus saine] .smallest[ ```javascript function reducer(state, action) { if (action.type === 'SET_ACTIVE_TAB') { return { ...state, // tabs: state.tabs activeTab: action.activeTab, }; } return state; } ``` ] -- .smallest[ ```javascript function reducer(state, action) { if ( action.type === 'SET_ACTIVE_TAB' && action.activeTab !== state.activeTab ) { return { ...state, // tabs: state.tabs activeTab: action.activeTab, }; } return state; } ``` ] --- # React avec Redux, en pratique ## 2.3 `Actions` Le `dispatch` d'une `action` Redux est _lourd_ de conséquences. -- - Exécutions des `middlewares` Redux. - Exécutions des `reducers` Redux. - Exécutions des `connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])`. - Re-rendus et cycle de vie éventuels des composants _connectés_ ainsi que leurs enfants. --- # React avec Redux, en pratique ## 2.3 `Actions` Attention aux `dispatch` d'`actions` _successifs_, en particulier dans les *actions de plus haut niveau*. .smaller[ ```javascript const setGlobalLoadingState = (loading) => (dispatch) => { dispatch(setSearchBarLoadingState(loading)); dispatch(setContentLoadingState(loading)); } const doSomethingImpactful = () => async (dispatch) => { const fetchPromise = fetchImportantData(); dispatch(setGlobalLoadingState(true)); let response; try { response = await fetchPromise; dispatch(processImportantData(response.data)); } finally { dispatch(setGlobalLoadingState(false)); return response; } } ``` ] --- # React avec Redux, en pratique ## 2.3 `Actions` Solutions envisageables: - Créer une nouvelle _action_ qui agira sur de multiples _reducers_. - Utiliser une librairie qui vous permettra de _gommer_ l'impact de tels patterns (telle que `redux-act`). -- .smaller[ ```javascript import { disbatch } from 'redux-act'; const setGlobalLoadingState = (loading) => (dispatch) => { disbatch( dispatch, [setSearchBarLoadingState(loading), setContentLoadingState(loading)] ); } ``` ] --- class: center, middle # Si vous avez raté le début... --- # TL*;*DR * comprendre ce qui se passe sous le capot permet de définir les règles... et de s'en affranchir * PureComponents et immutabilité * mémoiser partout, mais avec discernement * mesurer avant et après les changements * lire la documentation des outils que vous utilisez --- # Merci ! La présentation est disponible en ligne à l'adresse .smaller[https://julienw.github.io/presentation-react-perf-parisweb-2018/index.html] *Des questions ?*