Code splitting y lazy loading con Claude Code
Implementa code splitting en React y Next.js con Claude Code: ejemplos, pasos, riesgos, verificación y CTA.
Cuando una app de React o Next.js se siente lenta, muchas veces el problema está en el JavaScript inicial. El usuario entra para leer una página o pulsar un CTA, pero el navegador descarga también paneles de administración, editores, gráficos y widgets que no necesita todavía.
Code splitting significa dividir ese JavaScript en archivos más pequeños. Lazy loading significa cargar cada parte solo cuando hace falta. La idea es sencilla: no enviar toda la aplicación a cada visita inicial.
En esta guía verás cómo pedirle a Claude Code una implementación revisable, no solo una transformación automática. Usaremos React.lazy, Suspense, import() dinámico, next/dynamic, división por rutas o páginas, riesgos de hydration y una lista de verificación. Para completar el flujo, enlaza este tema con análisis de bundle, tree shaking y optimización de performance.
Conceptos básicos
Un bundle es el paquete de JavaScript enviado al navegador. Un chunk es un archivo más pequeño después de dividirlo. Un dynamic import, como import("./Chart"), carga un módulo en tiempo de ejecución. Suspense es el límite de React que muestra una UI temporal mientras se carga el componente.
hydration es el proceso en el que JavaScript del cliente conecta eventos y estado al HTML que ya vino del servidor. Si el servidor renderiza una cosa y el cliente otra, aparecen errores de hydration. Esto ocurre con facilidad cuando se leen window, localStorage, fechas o valores aleatorios demasiado pronto.
| Candidato | Por qué conviene | Cuidado con |
|---|---|---|
| Paneles de administración | No todos los usuarios los abren | Estados de permisos y carga |
| Gráficos, mapas, editores | Dependencias pesadas | Reservar altura para evitar saltos |
| Modales y asistentes | Solo aparecen tras una acción | Prefetch si el primer clic tarda |
| Video, audio o búsqueda | Suelen estar más abajo | Accesibilidad y mensajes de error |
Darle límites claros a Claude Code
No pidas solamente “haz code splitting”. Define objetivo, archivos y comprobaciones.
Objetivo:
- Cargar de forma diferida la UI pesada que no es necesaria al inicio.
- No mover hero, texto, navegación ni CTA detrás de lazy loading.
- Usar React.lazy/Suspense o next/dynamic según el framework.
Archivos objetivo:
- src/features/reports/ReportsPanel.tsx
- src/features/editor/RichEditor.tsx
- app/admin/page.tsx
Verificación:
- npm run lint
- npm run build
- Revisar initial JS y chunks diferidos en la pestaña Network.
- Revisar móvil para que el fallback no tape el CTA.
Así Claude Code trabaja con una superficie pequeña y no rompe contenido que genera confianza o ingresos.
React.lazy y Suspense
La documentación oficial de React explica que lazy retrasa la carga de un componente hasta su primer render, y Suspense muestra un fallback mientras tanto. Declara los componentes lazy en el nivel superior del módulo, no dentro de otro componente.
// src/App.tsx
import { Suspense, lazy, useState } from "react";
const ReportsPanel = lazy(() => import("./ReportsPanel"));
function PanelSkeleton() {
return (
<div role="status" aria-live="polite" style={{ minHeight: 180 }}>
Cargando reportes...
</div>
);
}
export default function App() {
const [showReports, setShowReports] = useState(false);
return (
<main>
<h1>Dashboard</h1>
<button type="button" onClick={() => setShowReports(true)}>
Ver reportes
</button>
{showReports ? (
<Suspense fallback={<PanelSkeleton />}>
<ReportsPanel />
</Suspense>
) : (
<p>La UI pesada de reportes se carga solo cuando hace falta.</p>
)}
</main>
);
}
// src/ReportsPanel.tsx
const rows = [
{ label: "Lectura completa", value: "68%" },
{ label: "Clics en CTA", value: "4.2%" },
{ label: "Visitas a consultoría", value: "1.1%" },
];
export default function ReportsPanel() {
return (
<section aria-label="Reportes">
<h2>Reporte de conversión</h2>
<ul>
{rows.map((row) => (
<li key={row.label}>
{row.label}: {row.value}
</li>
))}
</ul>
</section>
);
}
lazy espera un default export. Para named exports, adapta el módulo.
// src/lazyNamed.tsx
import { lazy, type ComponentType } from "react";
export function lazyNamed<TModule, TName extends keyof TModule>(
loader: () => Promise<TModule>,
name: TName
) {
return lazy(async () => {
const module = await loader();
return {
default: module[name] as ComponentType,
};
});
}
// src/AnalyticsSlot.tsx
import { Suspense } from "react";
import { lazyNamed } from "./lazyNamed";
const BarChart = lazyNamed(() => import("./charts"), "BarChart");
export function AnalyticsSlot() {
return (
<Suspense fallback={<p>Cargando gráfico...</p>}>
<BarChart />
</Suspense>
);
}
Dynamic import en Next.js
Next.js ya separa código por páginas y rutas. Usa next/dynamic cuando un Client Component sea pesado o dependa del navegador. El import() debe estar dentro de dynamic() y la declaración debe quedar en el nivel superior del módulo.
// app/admin/EditorSlot.tsx
"use client";
import dynamic from "next/dynamic";
const RichEditor = dynamic(() => import("./RichEditor"), {
ssr: false,
loading: () => (
<p aria-live="polite" style={{ minHeight: 160 }}>
Cargando editor...
</p>
),
});
export default function EditorSlot() {
return <RichEditor initialMarkdown="# Draft" />;
}
// app/admin/page.tsx
import EditorSlot from "./EditorSlot";
export default function AdminPage() {
return (
<main>
<h1>Editor de artículos</h1>
<p>El texto y el CTA se muestran primero. Solo se retrasa el editor pesado.</p>
<EditorSlot />
</main>
);
}
ssr: false sirve para editores o previews que usan APIs del navegador, pero no debe ocultar texto de venta, precios, FAQ, títulos o CTA principales.
Dividir por rutas y páginas
Antes de dividir cada componente, empieza por rutas. En Next.js, rutas como app/reports/page.tsx y app/settings/page.tsx ya separan el código de forma natural. En una SPA con React Router, puedes lazy-loadear los componentes de ruta.
// src/AppRouter.tsx
import { Suspense, lazy } from "react";
import { BrowserRouter, Link, Route, Routes } from "react-router-dom";
const HomePage = lazy(() => import("./pages/HomePage"));
const ReportsPage = lazy(() => import("./pages/ReportsPage"));
const SettingsPage = lazy(() => import("./pages/SettingsPage"));
function RouteFallback() {
return <p aria-live="polite">Cargando página...</p>;
}
export default function AppRouter() {
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link>
<Link to="/reports">Reports</Link>
<Link to="/settings">Settings</Link>
</nav>
<Suspense fallback={<RouteFallback />}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/reports" element={<ReportsPage />} />
<Route path="/settings" element={<SettingsPage />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
No encierres toda la página en un único Suspense si solo el gráfico es lento. Mantén visibles el título, la promesa y el CTA.
Casos de uso
Primero, dashboards SaaS. La navegación y el resumen se cargan de inmediato; analítica avanzada, logs y exportaciones se cargan después de verificar permisos.
Segundo, plataformas editoriales o cursos. El artículo, la lección y el CTA de compra deben renderizar primero. El editor Markdown, recorte de imágenes y vista previa pueden retrasarse.
Tercero, landing pages con mapas, gráficos, videos o calculadoras. El mensaje inicial aparece rápido y los widgets pesados cargan al hacer scroll o clic. Para UI multimedia, revisa video player y accesibilidad.
Cuarto, modales de cotización o checkout. El botón está disponible desde el inicio y el formulario de varios pasos se descarga bajo demanda.
Fallos frecuentes
- Dividir demasiado genera muchos requests pequeños y puede empeorar la interacción.
- Declarar
lazyodynamicdentro de un componente reinicia estado y dificulta el preloading. - Un fallback sin altura fija causa layout shift y puede tapar el CTA.
- Hydration mismatch aparece si servidor y cliente renderizan datos distintos.
- Poner contenido SEO o monetizable detrás de
ssr: falsereduce visibilidad y confianza.
Verificación
Pedido de revisión para Claude Code:
- Confirma que lazy/dynamic están en el nivel superior del módulo.
- Revisa default export frente a named export.
- Revisa si los límites de Suspense son demasiado amplios.
- Confirma que ssr:false no oculta texto SEO ni CTAs.
- Busca riesgos con window/date/random/localStorage.
- Explica cómo comparar initial JS, chunks lazy y número de requests.
npm run lint
npm run build
Después abre DevTools, filtra Network por JS, recarga y confirma que los chunks pesados no vienen en la carga inicial. Activa el reporte, editor o modal y verifica que el chunk aparezca solo entonces.
Enlaces oficiales y CTA
Consulta siempre las fuentes oficiales: React lazy, React Suspense, Next.js Lazy Loading y Next.js Layouts and Pages.
Para convertir esto en un flujo repetible, empieza con el cheatsheet gratuito de Claude Code. Para prompts reutilizables de implementación y revisión, usa 50 Claude Code Prompt Templates. Para equipos que necesitan reglas, CLAUDE.md y revisión de performance en repos reales, usa la consultoría y formación.
Nota práctica
La mejora fue más estable cuando se decidió antes qué no debía moverse: contenido inicial, navegación y CTA quedaron renderizados; reportes y editor pasaron a chunks lazy. Una petición vaga como “haz lazy loading” produjo límites de Suspense demasiado grandes y ssr: false innecesario.
PDF gratis: cheatsheet de Claude Code
Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.
Cuidamos tus datos y no enviamos spam.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.
Artículos relacionados
Escalera de permisos de Claude Code para ampliar acceso sin perder control
Pasa de read-only a ediciones limitadas, comandos de prueba y checks de deploy con menos riesgo.
Claude Code Small PR Proof Pack: cambios pequeños que sí se pueden revisar
Un paquete de prueba para PRs de Claude Code: diff, checks, URL pública, CTA y rollback.
Gate de revisión antes del commit con Claude Code
Cómo revisar con Claude Code antes del commit: diff, build, URL pública, Gumroad, consultoría, tests y archivos ajenos.