División de código
Bundling
La mayoría de las aplicaciones React tendrán sus archivos “empaquetados” o en un bundle con herramientas como Webpack, Rollup o Browserify. El bundling es el proceso de seguir los archivos importados y fusionarlos en un archivo único: un bundle o “paquete”. Este bundle se puede incluir en una página web para cargar una aplicación completa de una sola vez.
Ejemplo
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:
Tus bundles van a lucir muy diferente a esto.
Si usas Create React App, Next.js, Gatsby, o una herramienta similar, vas a tener una configuración de Webpack incluida para generar el bundle de tu aplicación.
Si no, tú mismo vas a tener que configurar el bundling. Por ejemplo, revisa las guías Installation y Getting Started en la documentación de Webpack.
División de código
El Bundling es genial, pero a medida que tu aplicación crezca, tu bundle también crecerá. Especialmente si incluyes grandes bibliotecas de terceros. Necesitas vigilar el código que incluyes en tu bundle, de manera que no lo hagas accidentalmente tan grande que tu aplicación se tome mucho tiempo en cargar.
Para evitar terminar con un bundle grande, es bueno adelantarse al problema y comenzar a dividir tu bundle. División de código es una funcionalidad disponible en bundlers como Webpack, Rollup y Browserify (vía factor-bundle) que puede crear múltiples bundles a ser cargados dinámicamente durante la ejecución de tu aplicación.
Dividir el código de tu aplicación puede ayudarte a cargar solo lo necesario en cada momento para el usuario, lo cual puede mejorar dramáticamente el rendimiento de tu aplicación. Si bien no habrás reducido la cantidad total de código en tu aplicación, habrás evitado cargar código que el usuario podría no necesitar nunca, y reducido la cantidad necesaria de código durante la carga inicial.
import()
La mejor manera de introducir división de código en tu aplicación es a través de la sintaxis de import()
dinámico.
Antes:
import { add } from './math';
console.log(add(16, 26));
Después:
import("./math").then(math => {
console.log(math.add(16, 26));
});
Cuando Webpack se encuentra esta sintaxis, comienza a dividir el código de tu aplicación automáticamente. Si estás usando Create React App, esto ya viene configurado para ti y puedes comenzar a usarlo. También es compatible por defecto en Next.js.
Si configuras Webpack por ti mismo, probablemente vas a querer leer la guía sobre división de código de Webpack. Tu configuración de Webpack debería verse vagamente como esta.
Cuando uses Babel, tienes que asegurarte de que Babel reconozca la sintaxis de import()
dinámico pero no la transforme. Para ello vas a necesitar @babel/plugin-syntax-dynamic-import.
React.lazy
La función React.lazy
te deja renderizar un import dinámico como un componente regular.
Antes:
import OtherComponent from './OtherComponent';
Después:
const OtherComponent = React.lazy(() => import('./OtherComponent'));
React.lazy
recibe una función que debe ejecutar un import()
dinámico. Este debe retornar una Promise
que se resuelve en un módulo con un export default
que contenga un componente de React.
El componente lazy debería entonces ser renderizado adentro de un componente Suspense
, lo que nos permite mostrar algún contenido predeterminado (como un indicador de carga) mientras estamos esperando a que el componente lazy cargue.
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
acepta cualquier elemento de React que quieras renderizar mientras esperas que OtherComponent
cargue. Puedes poner el componente Suspense
en cualquier parte sobre el componente lazy. Incluso puedes envolver múltiples componentes lazy con un solo 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>
);
}
Evitar el fallback
Cualquier componente puede suspenderse como resultado del renderizado, incluso componentes que ya se mostraron al usuario. Para que el contenido de la pantalla siempre sea consistente, si un componete que ya se ha mostrado se suspende, React trata de esconder su árbol hacia arriba hasta la barrera <Suspense>
más cercana. Sin embargo, desde la perspectiva del usuario esto puede desorientar.
Considera este componente para cambiar de pestaña:
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>
);
}
En este ejemplo, si la pestaña se cambia de 'photos'
a 'comments'
, pero Comments
se suspende, el usuario solo verá un destello. Esto tiene sentido porque el usuario no quiere ya ver Photos
, el componente Comments
aún no está listo para renderizar nada, y React necesita mantener la experiencia de usuario de forma consistente, así que no tiene otra opción que mostrar el componente Glimmer
de arriba.
Sin embargo, a veces esta experiencia de usuario no es deseable. En particular, a veces es mejor mostrar la IU “vieja” mientras se prepara la nueva IU. Puedes usar la nueva API startTransition
para que React haga esto:
function handleTabSelect(tab) {
startTransition(() => {
setTab(tab);
});
}
Aquí, le dices a React que poner la pestaña en 'comments'
no es una actualización urgente, sino que es una transición que puede tomar algún tiempo. React mantendrá entonces la IU anterior en su lugar y aún interactiva, y cambiará a mostrar <Comments />
cuando esté lista. Mira Transiciones para más información.
Límites de error
Si el otro módulo no se carga (por ejemplo, debido a un fallo de la red), se generará un error. Puedes manejar estos errores para mostrar una buena experiencia de usuario y manejar la recuperación con Límites de error. Una vez hayas creado tu límite de error (Error Boundary) puedes usarlo en cualquier parte sobre tus componentes lazy para mostrar un estado de error cuando haya un error de red.
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>
);
División de código basada en rutas
Decidir en qué parte de tu aplicación introducir la división de código puede ser un poco complicado. Quieres asegurarte de elegir lugares que dividan los bundles de manera uniforme, sin interrumpir la experiencia del usuario.
Un buen lugar para comenzar es con las rutas. La mayoría de la gente en la web está acostumbrada a que las transiciones entre páginas se tomen cierto tiempo en cargar. También tiendes a volver a renderizar todo de una vez, así que es improbable que tus usuarios interactúen con otros elementos en la página al mismo tiempo.
Este es un ejemplo de cómo configurar la división de código basada en rutas en tu aplicación usando bibliotecas como 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>
);
Exports con nombres
React.lazy
actualmente solo admite exports tipo default
. Si el módulo que desea importar utiliza exports con nombre, puede crear un módulo intermedio que lo vuelva a exportar como default
. Esto garantiza que el tree shaking siga funcionando y que no importes componentes no utilizados.
// 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"));