Code-Splitting
Impacchettamento
Molte applicazioni React hanno i loro file “impacchettati” usando strumenti come Webpack, Rollup oppure Browserify. Il processo di “impacchettamento” avviene prendendo tutti i file importati e unendoli tutti in un unico file, chiamato “bundle”. Questo file può essere poi incluso all’interno della pagina web per caricare l’intera applicazione tutta in una volta.
Esempio
App:
// app.js
import { add } from './math.js';
console.log(add(16, 26)); // 42
// math.js
export function add(a, b) {
return a + b;
}
Bundle:
function add(a, b) {
return a + b;
}
console.log(add(16, 26)); // 42
Nota:
I tuoi pacchetti avranno un aspetto molto diverso da questo.
Se state usando Create React App, Next.js, Gatsby, o qualche tool simile, potreste avere una configurazione di Webpack che effettua il processo d’impacchettamento della tua applicazione.
In caso contrario è necessario impostare l’impacchettamento dell’applicazione manualmente. Ad esempio puoi seguire queste guide sulla documentazione di Webpack, Installation e Getting Started.
Code Splitting
Il processo d’impacchettamento va bene, ma se l’applicazione cresce allora, di conseguenza, pure il bundle cresce, e questo specialmente se si includono librerie di terze parti. E’ necessario prestare attenzione al codice che si include all’interno del proprio bundle, in modo tale da non renderlo troppo pesante per non causare rallentamenti nella sua esecuzione.
Per evitare di avere un bundle enorme, è buona pratica iniziare a suddividere il proprio bundle. Il Code-Splitting (letteralmente “spezzamento del codice”) è una funzionalità supportata da Webpack, Rollup e Browserify (attraverso factor-bundle) i quali creano molteplici bundle che possono essere caricati dinamicamente a tempo di esecuzione.
Spezzare il codice della propria applicazione può aiutare ad effettuare il “lazy-load (caricamento pigro)” di funzionalità che sono necessarie in quel preciso istante, e questo processo può incrementare di parecchio le performance della propria applicazione. Anche se non è stato ridotto la quantità complessiva di codice dell’applicazione, abbiamo evitato di caricare codice di cui l’utente potrebbe non avere mai bisogno ed è stata ridotta la quantità di codice necessaria durante il caricamento iniziale.
import()
Il miglior modo per introdurre il code splitting all’interno dell’applicazione è attraverso la sintassi import()
.
Prima:
import { add } from './math';
console.log(add(16, 26));
Dopo:
import("./math").then(math => {
console.log(math.add(16, 26));
});
Quando Webpack incontra questa sintassi, inizia automaticamente il code splitting dell’applicazione. Se state utilizzando Create React App, questo è già automaticamente configurato per te e tu puoi iniziare ad utilizzarlo immediatamente. E’ anche supportato da Next.js.
Se state configurando Webpack autonomamente, probabilmente dovrai leggere la guida sul “code splitting” di Webpack. Il tuo webpack dovrebbe assomigliare a questo.
Quando usate Babel dovete assicurarvi che esso possa effettuare il parsing degli import dinamici. Per fare questo avete bisogno di @babel/plugin-syntax-dynamic-import.
React.lazy
La funzione React.lazy
ti permette di effettuare un import dinamico come se fosse un normale componente.
Prima:
import OtherComponent from './OtherComponent';
Dopo:
const OtherComponent = React.lazy(() => import('./OtherComponent'));
L’istruzione sopra carica il pezzo di codice contenente il componente OtherComponent
quando viene renderizzato per la prima volta.
La funzione React.lazy
prende in ingresso una funzione che, dinamicamente, chiama il metodo import()
. Quello che viene restituito è una Promise
che si risolve in un modulo contente l’export di default del componente React.
Il componente “pigro” viene poi renderizzato all’interno di un componente Suspense
il quale ci permette di mostrare dei contenuti di fallback (come ad esempio degli indicatori di caricamento) mentre stiamo aspettando che il componente sia completamente caricato.
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
La prop fallback
accetta un qualsiasi elemento React che si vuole renderizzare nel mentre stiamo aspettando che il componente sia caricato. E’ possibile mettere il componente Suspense
ovunque sopra il componente da caricare in modo lazy. E’ anche possibile circondare il componente da caricare in modo lazy col componente Suspense
.
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<section>
<OtherComponent />
<AnotherComponent />
</section>
</Suspense>
</div>
);
}
Evitare fallbacks
Ogni componente può essere sospeso a seguito di un rendering, anche componenti che erano già visibili all’utente. Per fare in modo che il contenuto dello schermo sia sempre consistente, se un componente già visibile viene sospeso, React deve nascondere il suo albero fino al più vicino <Suspense>
. Comunque, dal punto di vista dell’utente, ciò può creare confusione.
Considera questo tab switcher:
import React, { Suspense } from 'react';
import Tabs from './Tabs';
import Glimmer from './Glimmer';
const Comments = React.lazy(() => import('./Comments'));
const Photos = React.lazy(() => import('./Photos'));
function MyComponent() {
const [tab, setTab] = React.useState('photos');
function handleTabSelect(tab) {
setTab(tab);
};
return (
<div>
<Tabs onTabSelect={handleTabSelect} />
<Suspense fallback={<Glimmer />}>
{tab === 'photos' ? <Photos /> : <Comments />}
</Suspense>
</div>
);
}
In questo esempio, se il tab viene cambiato da 'photos'
a 'comments'
, ma Comments
viene sospeso, l’utente vedrà un glimmer. Ciò si spiega col fatto che l’utente non vuole più vedere Photos
, il componente Comments
non è ancora pronto per renderizzare nulla e React cerca di mantenere l’esperienza utente consistente, per questo non ha scelta che mostrare il Glimmer
.
Ad ogni modo, a volte tale esperienza utente non è desiderabile. In particolare, a volte è meglio mostrare la vecchia UI mentre la nuova UI viene preparata. Puoi usare la nuova API startTransition
in questo caso:
function handleTabSelect(tab) {
startTransition(() => {
setTab(tab);
});
}
Qui, dici a React che settare il tab a 'comments'
non è un aggiornamento importante, ma che si tratta di una transizione che può richiedere del tempo. React manterrà quindi la vecchia UI al suo posto ed interattiva, mostrerà <Comments />
quando sarà pronto. Dai uno sguardo a Transizioni per maggiori informazioni.
Contenitori di Errori
Se altri moduli falliscono nel caricamente (ad esempio a causa di problemi di rete), verrà sollevato un errore. Puoi gestire questi errori per mostrare un Contenitore di Errori. Una volta che è stato creato il proprio contenitore di errori, è possibile utilizzarlo ovunque sopra i componenti lazy per mostrare uno stato di errore quando ci sono errori di rete.
import React, { Suspense } from 'react';
import MyErrorBoundary from './MyErrorBoundary';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));
const MyComponent = () => (
<div>
<MyErrorBoundary>
<Suspense fallback={<div>Loading...</div>}>
<section>
<OtherComponent />
<AnotherComponent />
</section>
</Suspense>
</MyErrorBoundary>
</div>
);
Code splitting basato su rotte
Decidere dove, nella propria app, introdurre il code splitting può essere complicato. Assicurati di scegliere posti che divideranno i pacchetti in modo uniforme, senza diminuire l’esperienza dell’utente.
Un buon posto per iniziare sono le rotte. La maggior parte delle persone sul Web è abituata a caricare le transizioni di pagina che richiedono un po ‘di tempo. Tendi anche a rieseguire il rendering dell’intera pagina contemporaneamente, quindi è improbabile che i tuoi utenti interagiscano contemporaneamente con altri elementi della pagina.
Qui di seguito possiamo vedere un esempio code splitting, basato sulle rotte, utilizzando libreria come React Router con React.lazy
.
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const App = () => (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
</Router>
);
Named Exports
Ad oggi React.lazy
supporta solamente gli export di default. Se il modulo che vogliamo importare utilizza export nominali, possiamo creare un modulo intermedio che lo re-esporta come default. In questo modo ci assicuriamo che il tree shaking continui a funzionare e che non vengano caricati componenti non utilizzati.
// ManyComponents.js
export const MyComponent = /* ... */;
export const MyUnusedComponent = /* ... */;
// MyComponent.js
export { MyComponent as default } from "./ManyComponents.js";
// MyApp.js
import React, { lazy } from 'react';
const MyComponent = lazy(() => import("./MyComponent.js"));