Optimisez votre application React avec Loadable Components
Loadable Components est une librairie permettant de faire du Code Splitting avec React. Autrement dit, elle vous permet de décaler le chargement de certains composants afin d'alléger la taille de vos bundles et donc d'améliorer la vitesse de chargement de votre application.
Code Splitting
Il y a de ça quelques années, lorsque l'on développait un site internet il était commun de faire un fichier JavaScript par page : "home.js", "article.js", "about.js"...
Aujourd'hui le monde a changé, nous créons des applications avec énormément de fichiers. Cela nous force à utiliser des bundlers tels que Webpack ou Parcel pour assembler tous ces fichiers et en faire un seul unique gros fichier que l'on nomme "bundle". Ce fichier est en général assez lourd, il contient tout le JavaScript de votre site internet. D'un côté c'est très pratique mais d'un autre côté, on fait charger à l'utilisateur du JavaScript dont il n'a pas besoin.
C'est là qu'intervient le Code Splitting, cela consiste à faire en sorte que votre bundler ne génère pas un bundle mais plusieurs. Il existe plusieurs techniques pour y arriver mais la plus simple est d'utiliser des imports dynamiques.
Le "dynamic import" est une nouvelle syntaxe qui sera intégrée au langage prochainement, mais nos bundlers préférés la comprennent déjà 😉.
L'idée est très simple, vous appelez une fonction asynchrone import()
et elle vous renvoie le module correspondant :
// utils.js
export const hello = () => console.log('hello')
// main.js
import('./utils')
.then(({ hello }) => hello())
.catch(err => console.error('Impossible to load ./utils'))
En passant ça dans Webpack, vous obtiendrez donc deux fichiers en sortie.
bundle.js
: le bundle contenant./main.js
0.js
: le bundle correspondant à./utils.js
Au lieu d'avoir utils.js
intégré à votre bundle.js
, il est chargé à la demande lorsque vous en avez besoin. bundle.js
est donc plus petit et qui dit plus petit, dit chargé plus rapidement, qui dit chargé plus rapidement dit une page plus rapidement intéractive, qui dit une page plus rapidement intéractive dit plus d'utilisateurs, qui dit plus d'utilisateurs dit 💸 😏.
Loadable Components
Maintenant que vous avez compris ce qu'est le Code Splitting, venons-en au sujet principal de cet article : le Component Splitting avec Loadable Components.
Loadable Components vous permet de charger dynamiquement un composant. J'ai créé la librairie avec un seul but en tête : la simplicité d'intégration et d'utilisation.
Pour splitter un composant, il vous suffit d'appeler la fonction loadable()
avec l'import de votre composant :
import loadable from 'loadable-components'
const Article = loadable(() => import('./Article'))
const App = () => (
<main>
<h1>Welcome on my website</h1>
<Article />
</main>
)
L'exemple ci-dessus affichera le composant "./Article" de manière différée (j'ai rempli le contrat pour la simplicité non 😅) ?
Route Splitting
Si vous faites du web, il est probable que votre application soit architecturée autour de routes, avec React Router par exemple.
Le Code Splitting par route est en général un bon choix pour plusieurs raisons :
- Le code est déjà bien séparé
- L'utilisateur s'attend à un chargement lorsqu'il clique sur un lien
Pour faire du Code Splitting par route, je vous conseille de commencer par créer un fichier Routes.js
qui contiendra tous les composants correspondants aux routes de votre application. Ainsi vous pourrez décider facilement si une route doit être chargée dynamiquement ou non.
// Routes.js
import loadable from 'loadable-components'
// La Home est critique, elle sera dans le bundle principale
export { default as Home } from './Home'
// Les routes About et Contact seront chargées au besoin
export const About = loadable(() => import('./About'))
export const Contact = loadable(() => import('./Contact'))
Il vous suffit ensuite de faire pointer vos routes vers les composants :
// App.js
import React from 'react'
import { Route } from 'react-router'
import * as Routes from './Routes'
export default () => (
<div>
<Route exact path="/" component={Routes.Home} />
<Route path="/about" component={Routes.About} />
<Route path="/contact" component={Routes.Contact} />
</div>
)
Cette technique est très efficace, l'article que vous lisez actuellement a été affiché via un composant splitté 😎.
Preloading
Le Code Splitting c'est bien, l'affichage de votre page est plus rapide mais en contrepartie votre utilisateur a un chargement. Et bien non, si vous chargez le composant avant même qu'il l'ait demandé, vous pouvez éviter ce temps de chargement.
Et encore une fois, avec Loadable Components c'est très facile, il vous suffit d'appeler la fonction load()
sur votre composant :
// Routes.js
import loadable from 'loadable-components'
// La Home est critique, elle sera dans le bundle principale
export { default as Home } from './Home'
// Les routes About et Contact seront chargées au besoin
export const About = loadable(() => import('./About'))
export const Contact = loadable(() => import('./Contact'))
// Préchargement des routes
About.load()
Contact.load()
Vous pouvez également choisir de déclencher le chargement à un autre moment, au "hover" sur un lien par exemple :
import React from 'react'
import { Contact } from './Routes'
const Links = () => (
<div>
<Link to="/contact" onMouseOver={Contact.load}>
Contact
</Link>
</div>
)
Custom Loading
Par défaut Loadable Components n'affiche rien pendant le chargement, mais vous pouvez choisir d'afficher un joli loader qui scintille ✨.
const Loading = () => <div>Loading...</div>
const About = loadable(() => import('./About'), {
LoadingComponent: Loading,
})
Et pour ceux qui préfèrent avoir le contrôle total, la fonction render
:
import React from 'react'
const About = loadable(() => import('./About'), {
render: ({ Component, loading, ownProps }) => {
if (loading) return <div>Loading... ✨</div>
return <Component {...ownProps} />
},
})
Custom Error
C'est le même principe pour les erreurs de chargement, vous pouvez afficher ce que vous voulez.
const ErrorDisplay = ({ error }) => <div>Oups! {error.message} 💥</div>
const Home = loadable(() => import('./Home'), {
ErrorComponent: ErrorDisplay,
})
Toujours la fonction render
pour les plus aguéris :
import React from 'react'
const About = loadable(() => import('./About'), {
render: ({ Component, error, ownProps }) => {
if (error) return <div>Oups! {error.message}</div>
return <Component {...ownProps} />
},
})
It's just a Promise
Le focus principal de la librairie est d'être simple c'est vrai, mais le second c'est la flexibilité. L'unique contrat à respecter est de retourner un composant dans la fonction que vous passez à loadable()
. À partir de là, les possibilités sont infinies.
Imaginons que vous ayez besoin d'un gros fichier JSON dynamique pour que votre composant s'affiche :
import React from 'react'
import loadable from 'loadable-components'
const loadBigTable = jsonFile =>
loadable(async () => {
const [{ default: Books }, { default: json }] = await Promise.all([
import('./BigTable'),
import(jsonFile),
])
return props => <BigTable {...props} content={json} />
})
const BooksTable = loadBigTable('./books.json')
const MoviesTable = loadBigTable('./movies.json')
L'exemple ci-dessous va chercher le composant et ses ressources en parallèle, en quelques lignes on a résolu un problème compliqué !
Server Side Rendering
Le Code Splitting c'est vraiment cool, c'est ce que vous vous dites tant que vous ne l'avez pas couplé avec le Server Side Rendering 😒. Mais Jamy dit-nous, pourquoi est-ce compliqué de mixer du Code Splitting et du Server Side Rendering ?
- Node.js ne supporte pas les dynamic imports
- React doit avoir toutes les ressources avant de démarrer le rendu de l'application, donc tous les composants
- Le client doit attendre d'avoir chargé toutes les ressources pour ne pas faire subir à l'utilisateur un "blink"
Tous ces points font que c'est compliqué, et même très compliqué 😰. Heureusement Loadable Components est là pour vous simplifier la vie !
On a donc trois étapes pour mettre en place le Server Side Rendering :
1. Configurer Babel
{
"plugins": ["loadable-components/babel"]
}
2. Récupérer et charger les modules côté serveur
import React from 'react'
import { renderToString } from 'react-dom/server'
import { getLoadableState } from 'loadable-components/server'
import App from './App'
const app = <App />
// Extract loadable state from application tree
getLoadableState(app).then(loadableState => {
const html = renderToString(app)
// Insert script tag into page
const page = `
<!doctype html>
<html>
<body>
<div id="main">${html}</div>
${loadableState.getScriptTag()}
</body>
</html>
`
})
3. Charger les composants côté client
import React from 'react'
import { hydrate } from 'react-dom'
import { loadComponents } from 'loadable-components'
import App from './App'
// Load all components needed before starting rendering
loadComponents().then(() => hydrate(<App />, document.getElementById('main')))
Voici les étapes nécessaires au fonctionnement de Loadable Components côté serveur, c'est un peu compliqué par rapport au reste, mais on ne peut pas faire plus simple pour le moment !
Conclusion
Je pense qu'après avoir lu cet article vous n'avez plus aucune excuse pour ne pas faire de Component Splitting dans votre application React 😇 ! J'ai été volontairement bref sur les possibilités offertes par Loadable Components mais vous trouverez la documentation complète de Loadable Components sur GitHub.