Técnica de lazy load en las rutas

Introducción al problema

En este punto de la aplicación ya tienes varias rutas montadas: página de inicio, página de búsqueda, detalle de empleo, paginación, estilos específicos, etc. Todo funciona perfectamente, pero hay un problema importante de rendimiento:

Ahora mismo estás cargando toda la aplicación nada más entrar en la web, aunque el usuario solo vea la página de inicio.

Eso implica que se descargan:

  • Código de la página de búsqueda
  • Código de la página de detalle
  • Componentes como la paginación o el listado de empleos
  • Estilos específicos de páginas que quizá el usuario nunca visite

La consecuencia: mucho JavaScript descargado que no se usa en el primer render.

El objetivo de esta clase es aprender a aplicar lazy load para que solo cargues el código de las rutas cuando el usuario realmente las necesita.

Midiendo el problema con Coverage

Antes de optimizar, es importante demostrar que hay un problema. Para ello usamos la herramienta Coverage de las DevTools del navegador.

Pasos para usar Coverage

  1. Abre las DevTools del navegador
  2. Ve al panel de Coverage
  3. Recarga la página de inicio de la aplicación
  4. Observa la lista de archivos JavaScript y CSS que se han cargado

En el vídeo se ve algo como:

  • Se está cargando el archivo de la página de Detail
  • El archivo de Search
  • Componentes como Pagination
  • Estilos de páginas que ni siquiera se están mostrando

Y el dato clave:

Hay archivos donde solo se está utilizando un 50 % o un 36 % del código cargado en la página actual

Es decir, estás cargando cosas que no necesitas todavía.

La regla de oro de la optimización

Aquí aparece una idea muy importante que se repite varias veces en la clase:

Solo carga aquello que necesites. No cargues más.

Esta regla aplica a todo:

  • Web
  • Backend
  • Bases de datos
  • Y, por supuesto, a aplicaciones en React

Nuestro problema en React es claro: estamos importando todas las páginas de golpe en App.jsx.

Qué es lazy load en React

Lazy load significa cargar algo solo cuando se necesita.

Aplicado a React, es:

  • No descargar el código de una página hasta que el usuario navega a esa ruta
  • Dividir el bundle en trozos más pequeños para que el primer acceso sea mucho más rápido

React nos da dos piezas clave para esto:

  • React.lazy - para hacer imports dinámicos de componentes
  • Suspense - para mostrar un contenido de espera mientras se carga el componente perezoso

Situación inicial en App.jsx

El código original de App.jsx suele tener algo parecido a esto:

// app.jsx
import { HomePage } from './pages/Home.jsx'
import { SearchPage } from './pages/Search.jsx'
import { JobDetailPage } from './pages/JobDetail.jsx'
import { NotFoundPage } from './pages/NotFound.jsx'

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/search" element={<SearchPage />} />
        <Route path="/job/:id" element={<JobDetailPage />} />
        <Route path="*" element={<NotFoundPage />} />
      </Routes>
    </Router>
  )
}

Problema: En cuanto se renderiza App, todas las páginas se importan y se descargan, aunque el usuario nunca llegue a verlas.

Aplicando lazy load con React.lazy

El primer paso es importar lo que necesitamos desde React:

// app.jsx
import { lazy, Suspense } from 'react'

Ahora cambiamos los imports estáticos por imports perezosos:

// Definimos las páginas como componentes lazy
const HomePage = lazy(() => import('./pages/Home.jsx'))
const SearchPage = lazy(() => import('./pages/Search.jsx'))
const JobDetailPage = lazy(() => import('./pages/JobDetail.jsx'))
const NotFoundPage = lazy(() => import('./pages/NotFound.jsx'))

Y mantenemos las rutas como antes:

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/search" element={<SearchPage />} />
        <Route path="/job/:id" element={<JobDetailPage />} />
        <Route path="*" element={<NotFoundPage />} />
      </Routes>
    </Router>
  )
}

¿Qué va a pasar ahora?

  • Cuando la app se carga por primera vez, solo se descarga el código necesario para renderizar la home
  • Cuando el usuario va a /search, el navegador hace un import dinámico de la página de búsqueda y sus dependencias
  • Cuando va al detalle, hace otro import dinámico solo de lo necesario para esa vista

El error raro: “Cannot convert an object to a primitive value”

En el vídeo, justo después de aplicar React.lazy, al probar la app aparece un error bastante feo:

Cannot convert an object to a primitive value

Es un error muy poco descriptivo, difícil de entender a primera vista. Pero la causa es esta:

React.lazy espera que el import() devuelva un módulo donde el componente principal está exportado como default

En nuestro proyecto, las páginas están exportadas como exportaciones nombradas, algo así:

// pages/Home.jsx
export function HomePage() {
  // ...
}

React.lazy estaría recibiendo algo como { HomePage: function HomePage() {} } y no sabe qué hacer con eso por defecto.

Ajustando las páginas para que funcionen con lazy load

La solución sencilla que se aplica en el vídeo es añadir una exportación por defecto en las páginas.

Por ejemplo, en pages/Home.jsx:

// pages/Home.jsx
function HomePage() {
  // código de la página
}

export default HomePage

O, si ya la tienes como función exportada nombrada:

export function HomePage() {
  // código de la página
}

export default HomePage

Haz lo mismo para:

  • SearchPage
  • JobDetailPage
  • NotFoundPage

De esta forma, cuando hagamos:

const HomePage = lazy(() => import('./pages/Home.jsx'))

React.lazy recibirá correctamente el componente por defecto que espera.

:::tip[Nota para el alumno] Hay formas de mantener exportaciones nombradas con lazy load usando .then(module => ({ default: module.HomePage })), pero en esta clase se simplifica usando export default para centrarse en entender el lazy load. :::

Verificando la mejora con Coverage

Volvemos a la herramienta Coverage para comprobar que la optimización funciona.

  1. Recarga la página de inicio tras aplicar lazy load
  2. Mira de nuevo la lista de archivos

Ahora deberías ver algo así:

Se cargan archivos como:

  • home
  • app
  • header
  • link

Ya NO se cargan:

  • search
  • detail
  • pagination

Es decir:

  • La home se convierte en un bundle mucho más pequeño
  • El resto de código se descarga solo cuando navegas a esas páginas

Qué pasa cuando el usuario navega

Cuando el usuario:

Va a /search y hace una búsqueda, se descargan:

  • La página de búsqueda
  • La paginación
  • El listado de empleos
  • Sus estilos asociados

Luego, al entrar al detalle de un empleo, se descarga:

  • La página de detalle
  • La librería de Markdown
  • El CSS específico de detail

Además, una vez que el usuario ha navegado por esas páginas, el código ya está en memoria y:

  • Las siguientes navegaciones son mucho más rápidas
  • El navegador ya no vuelve a descargar ese JavaScript

Mejorando la experiencia de usuario con Suspense

Hay un pequeño detalle a tener en cuenta.

El problema

¿Qué pasa si:

  • El usuario tiene una conexión lenta
  • Hace clic en “Buscar”
  • O navega al detalle

Mientras se descargan los bundles perezosos, hay un pequeño tiempo muerto. Por defecto, React mantiene la página anterior hasta que la nueva está lista, lo que puede ser un poco confuso.

La solución: Suspense con fallback

Podemos envolver las rutas que usan lazy load con el componente Suspense y mostrar algo mientras se cargan:

import { lazy, Suspense } from 'react'

const HomePage = lazy(() => import('./pages/Home.jsx'))
const SearchPage = lazy(() => import('./pages/Search.jsx'))
const JobDetailPage = lazy(() => import('./pages/JobDetail.jsx'))
const NotFoundPage = lazy(() => import('./pages/NotFound.jsx'))

function App() {
  return (
    <Router>
      <Header />

      <Suspense fallback={<p>Cargando página...</p>}>
        <Routes>
          <Route path="/" element={<HomePage />} />
          <Route path="/search" element={<SearchPage />} />
          <Route path="/job/:id" element={<JobDetailPage />} />
          <Route path="*" element={<NotFoundPage />} />
        </Routes>
      </Suspense>
    </Router>
  )
}

Ahora, cuando el usuario:

  • Va a la página de búsqueda con una conexión lenta
  • O entra al detalle de un empleo

Entre la página anterior y la nueva se verá un fallback de cargando, que puede ser:

  • Un texto sencillo
  • Un spinner
  • Un skeleton
  • Incluso un diseño distinto según la ruta, si quisieras afinarlo más adelante

:::note En el vídeo se simula una conexión más lenta y se ve cómo el fallback aparece solo cuando la carga tarda lo suficiente. :::

Resumen de la clase

En esta clase has aprendido a:

  1. Detectar código no utilizado en la primera carga usando la herramienta Coverage del navegador

  2. Entender la regla de oro de la optimización:

    Solo carga aquello que necesites

  3. Aplicar lazy load con:

    React.lazy(() => import('./pages/...'))
  4. Resolver el error de React.lazy:

    • Añadiendo export default a las páginas para que el import dinámico funcione
  5. Comprobar el impacto:

    • La página de inicio descarga mucho menos código
    • Las demás rutas se cargan solo cuando el usuario navega hacia ellas
  6. Mejorar la experiencia de usuario con Suspense:

    • Añadiendo un fallback para mostrar algo mientras se descargan los bundles perezosos

Antes cargabas 100 KB nada más entrar. Ahora puedes dividirlos en varios trozos y cargar solo 30 y pico KB para la home. El resto se descarga bajo demanda según el usuario navega. Más rápido, más eficiente y con mejor experiencia para el usuario.