Écrit par Abdourahmane S.
React a gagné en popularité en tant que l’une des bibliothèques JavaScript les plus prisées pour la création d’interfaces utilisateur, grâce à sa structure basée sur des composants et à son mécanisme de rendu efficace. Cependant, à mesure que votre application React prend de l’ampleur, il devient crucial de se concentrer sur l’optimisation des performances. Dans cet article, nous explorerons les meilleures pratiques pour optimiser les performances des applications React, en présentant des exemples concrets pour vous aider à développer des applications web plus rapides et plus réactives.
L’importance de l’optimisation des performances
- Expérience Utilisateur Améliorée : Des performances optimisées se traduisent par une expérience utilisateur plus fluide et agréable.
- Réduction des Temps de Chargement : Des temps de chargement plus courts permettent aux utilisateurs d’accéder rapidement au contenu de l’application.
- Réactivité Accrue : Une application réactive répond plus rapidement aux actions de l’utilisateur, améliorant ainsi son interactivité.
- Utilisation Efficace des Ressources : L’optimisation des performances permet une utilisation efficace des ressources du système, ce qui peut réduire les coûts d’infrastructure.
- Rétention des Utilisateurs : Des performances optimales contribuent à fidéliser les utilisateurs, les incitant à revenir sur l’application.
- Succès du Projet : Des performances de haut niveau sont souvent associées au succès et à la compétitivité d’un projet dans un marché concurrentiel.
Comment optimiser une application React ?
Utilisation de React.memo() # :
React.memo() est un composant d’ordre supérieur qui optimise le rendu des composants fonctionnels en mémorisant leur état. Il empêche les re-rendus inutiles en vérifiant si les propriétés (props) ont changé par rapport au rendu précédent. Si les props n’ont pas changé, le composant ne se re-rend pas.
import React, { useState, memo } from 'react';
// Composant pour afficher le nombre de tâches terminées
const CompletedTasks = ({ count }) => {
console.log('Rendering CompletedTasks');
return <div>Completed Tasks: {count}</div>;
};
// Mémoriser le composant pour éviter les re-rendus inutiles
const MemoizedCompletedTasks = memo(CompletedTasks);
const TaskApp = () => {
const [tasks, setTasks] = useState([
{ id: 1, text: 'Review de PR sur Artemis', completed: true },
{ id: 2, text: 'Ecrire les tests unitaires sur Osmie', completed: false },
{ id: 3, text: 'Peer programming', completed: false },
]);
const toggleTaskCompletion = (taskId) => {
setTasks(tasks.map(task =>
task.id === taskId ? { ...task, completed: !task.completed } : task
));
};
const completedCount = tasks.filter(task => task.completed).length;
return (
<div>
<h1>Task List</h1>
<ul>
{tasks.map(task => (
<li key={task.id}>
<span
style={{ textDecoration: task.completed ? 'line-through' : 'none' }}
onClick={() => toggleTaskCompletion(task.id)}
>
{task.text}
</span>
</li>
))}
</ul>
<MemoizedCompletedTasks count={completedCount} />
</div>
);
};
export default TaskApp;
Utilisation du code splitting # :
Le découpage du code (code splitting) permet de diviser votre application en morceaux plus petits, chargeant uniquement le code nécessaire pour une route ou une fonctionnalité spécifique. Cela réduit le temps de chargement initial et améliore les performances.
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
// Utilisation de lazy pour charger les composants de manière asynchrone
const Home = lazy(() => import('./Home'));
const About = lazy(() => import('./About'));
const App = () => {
return (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
</Switch>
</Suspense>
</Router>
);
};
export default App;
- Lazy : La fonction React.lazy() permet de charger un composant de manière asynchrone. Chaque composant (Home et About) est chargé uniquement lorsque la route correspondante est accédée.
- Suspense : Le composant Suspense est utilisé pour afficher un fallback (ici, un simple message “Loading…”) pendant que les composants asynchrones sont en cours de chargement.
- Router : BrowserRouter (nommé ici Router) et Switch de react-router-dom sont utilisés pour gérer les routes de l’application.
PureComponent # :
Si vous utilisez des composants de classe, envisagez d’utiliser PureComponent au lieu de Component. PureComponent effectue une comparaison superficielle des props et de l’état pour déterminer si un composant doit être mis à jour.
import React, { PureComponent } from 'react';
class MyComponent extends PureComponent {
render() {
console.log('Rendering MyComponent');
return <div>Hello, {this.props.name}!</div>;
}
}
class App extends PureComponent {
state = {
name: 'Haouili',
};
changeName = () => {
this.setState({ name: 'Haouili' }); // Même nom, donc pas de changement réel
};
render() {
return (
<div>
<button onClick={this.changeName}>Change Name</button>
<MyComponent name={this.state.name} />
</div>
);
}
}
export default App;
- Sans PureComponent, MyComponent se re-rend chaque fois que le bouton est cliqué, même si le nom n’a pas changé.
- Avec PureComponent, MyComponent se re-rend uniquement si name change réellement, améliorant ainsi les performances en évitant les re-rendus inutiles.
Optimisation des rendus coûteux avec useMemo et useCallback # :
Le hook useMemo permet de mémoriser des calculs coûteux, tandis que useCallback mémorise des fonctions pour éviter leur recréation inutile. Utilisez-les lorsque cela est nécessaire pour optimiser les composants critiques en termes de performance.
import React, { useState, useMemo, useCallback } from 'react';
// Composant pour afficher une liste de nombres triés
const SortedList = ({ list }) => {
const sortedList = useMemo(() => {
console.log('Tri de la liste');
return [...list].sort((a, b) => a - b);
},
);
return (
<div>
<h2>Liste triée</h2>
<ul>
{sortedList.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
};
// Composant principal pour gérer et ajouter des éléments à la liste
const App = () => {
const
= useState([5, 3, 8, 1]);
const [number, setNumber] = useState('');
const addNumber = useCallback(() => {
setList((prevList) => [...prevList, parseInt(number)]);
setNumber('');
}, [number]);
return (
<div>
<h1>Liste de nombres </h1>
<input
type="text"
value={number}
onChange={(e) => setNumber(e.target.value)}
placeholder="Entrez un nombre"
/>
<button onClick={addNumber}>Ajouter le nombre</button>
<SortedList list={list} />
</div>
);
};
export default App;
- Le composant SortedList affiche la liste triée. Grâce à useMemo, la liste est triée uniquement lorsque la liste change, évitant des tris inutiles lors de chaque rendu.
- Le hook useCallback garantit que la fonction addNumber ne change que lorsque c’est nécessaire, évitant ainsi des ré-rendus inutiles des composants enfants dépendant de cette fonction.
Utilisation de Reselect # :
Reselect est une bibliothèque qui offre une façon élégante de calculer les données dérivées dans un environnement Redux. Il utilise des sélecteurs mémorisés pour extraire des données spécifiques du store Redux et les mémoriser jusqu’à ce que les dépendances changent.
Installation :
npm install redux reselect
import { createSelector } from 'reselect';
// Sélecteur pour récupérer les tâches du store Redux
const getTasks = state => state.tasks;
// Sélecteur pour filtrer les tâches terminées
export const getCompletedTasks = createSelector(
[getTasks],
tasks => tasks.filter(task => task.completed)
);
// Sélecteur pour compter le nombre de tâches terminées
export const getCompletedTaskCount = createSelector(
[getCompletedTasks],
completedTasks => completedTasks.length
);
- getCompletedTasks : Ce sélecteur filtre les tâches terminées à partir de la liste complète des tâches.
- getCompletedTaskCount : Ce sélecteur compte le nombre de tâches terminées en utilisant le résultat du sélecteur précédent.
Ces sélecteurs sont mémorisés, ce qui signifie qu’ils ne seront recalculés que si les données qu’ils utilisent changent.
Utilisation dans un composant Redux :
import React from 'react';
import { connect } from 'react-redux';
import { getCompletedTaskCount } from './selectors';
const TaskCounter = ({ completedTaskCount }) => (
<div>Nombre de tâches terminées : {completedTaskCount}</div>
);
const mapStateToProps = state => ({
completedTaskCount: getCompletedTaskCount(state)
});
export default connect(mapStateToProps)(TaskCounter);
Conclusion
Optimiser les performances d’une application est essentiel pour garantir une expérience utilisateur fluide et réactive. Que ce soit en réduisant les temps de chargement initiaux, en minimisant les ré-rendus inutiles, ou en optimisant les calculs coûteux, chaque amélioration apportée contribue à une meilleure expérience globale.
En adoptant des pratiques telles que le chargement différé (lazy loading), la mémorisation des calculs avec des outils comme useMemo et useCallback, ou l’utilisation de bibliothèques spécialisées comme Reselect avec Redux, les développeurs peuvent optimiser leurs applications et garantir des performances optimales.
Cependant, il est également important de garder à l’esprit que l’optimisation des performances ne doit pas compromettre la lisibilité, la maintenabilité ou la qualité du code. Une approche équilibrée qui tient compte à la fois des besoins fonctionnels et des exigences de performances est essentielle pour créer des applications robustes et performantes.