feat: implementar lazy loading y code splitting para optimización

- Convertir todas las páginas (excepto Home y NotFound) a lazy loading con React.lazy
- Añadir Suspense con PageLoader como fallback
- Configurar code splitting en vite.config.ts:
  - Separar vendor-react (252 kB)
  - Separar vendor-markdown (114 kB)
  - Separar vendor-query, vendor-ui, vendor-icons, etc.
  - Separar cada página en chunk individual
- Bundle inicial reducido de ~368 kB a 3.29 kB (1.25 kB gzipped)
- Mejora significativa en tiempo de carga inicial
- Páginas se cargan bajo demanda al navegar
This commit is contained in:
planetazuzu 2025-12-20 23:16:23 +01:00
parent 1ae83f36bf
commit 7f85eba09c
5 changed files with 151 additions and 49 deletions

View file

@ -12,6 +12,8 @@
## 1. OBJETIVO OPERATIVO ## 1. OBJETIVO OPERATIVO
![Cánulas de Guedel y nasofaríngea](/assets/infografias/bloque-3-material-sanitario/canulas-guedel-nasofaringea.png)
Insertar cánula orofaríngea (OPA) de forma segura y efectiva en pacientes inconscientes sin reflejo nauseoso, manteniendo vía aérea permeable para facilitar ventilación e integrando con **ventilación con bolsa-mascarilla (3.1) y evaluación primaria ABCDE (1.2)**. Insertar cánula orofaríngea (OPA) de forma segura y efectiva en pacientes inconscientes sin reflejo nauseoso, manteniendo vía aérea permeable para facilitar ventilación e integrando con **ventilación con bolsa-mascarilla (3.1) y evaluación primaria ABCDE (1.2)**.
Este capítulo se centra en **técnica operativa de inserción de OPA**, no en dispositivos avanzados de vía aérea. Este capítulo se centra en **técnica operativa de inserción de OPA**, no en dispositivos avanzados de vía aérea.

View file

@ -10,6 +10,10 @@
## 3.3.1 Objetivo operativo ## 3.3.1 Objetivo operativo
![Uso correcto de la bolsa-mascarilla (Ambú)](/assets/infografias/bloque-3-material-sanitario/uso-correcto-ambu.png)
![Configuración máxima de FiO2 con bolsa-mascarilla](/assets/infografias/bloque-3-material-sanitario/configuracion-maxima-fio2-bolsa-mascarilla.png)
Usar la BVM de forma **segura y eficaz** para ventilación asistida básica, integrándola con **oxigenoterapia (3.03.1), aspiración (3.2) y traslado**, minimizando **fugas, insuflación gástrica y pérdida de control**. Usar la BVM de forma **segura y eficaz** para ventilación asistida básica, integrándola con **oxigenoterapia (3.03.1), aspiración (3.2) y traslado**, minimizando **fugas, insuflación gástrica y pérdida de control**.
Este capítulo se centra en **material, montaje, técnica de sellado y coordinación**, no en ventilación avanzada, fármacos o decisiones clínicas complejas. Este capítulo se centra en **material, montaje, técnica de sellado y coordinación**, no en ventilación avanzada, fármacos o decisiones clínicas complejas.

View file

@ -1,4 +1,4 @@
import { useState } from 'react'; import { useState, Suspense, lazy } from 'react';
import { Toaster } from "@/components/ui/toaster"; import { Toaster } from "@/components/ui/toaster";
import { Toaster as Sonner } from "@/components/ui/sonner"; import { Toaster as Sonner } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
@ -12,28 +12,35 @@ import SearchModal from "@/components/layout/SearchModal";
import MenuSheet from "@/components/layout/MenuSheet"; import MenuSheet from "@/components/layout/MenuSheet";
import UpdateNotification from "@/components/layout/UpdateNotification"; import UpdateNotification from "@/components/layout/UpdateNotification";
import InstallBanner from "@/components/layout/InstallBanner"; import InstallBanner from "@/components/layout/InstallBanner";
import Home from "./pages/Index";
import SoporteVital from "./pages/SoporteVital";
import Patologias from "./pages/Patologias";
import Escena from "./pages/Escena";
import Farmacos from "./pages/Farmacos";
import Herramientas from "./pages/Herramientas";
import Material from "./pages/Material";
import Telefono from "./pages/Telefono";
import Comunicacion from "./pages/Comunicacion";
import ManualIndex from "./pages/ManualIndex";
import ManualViewer from "./pages/ManualViewer";
import NotFound from "./pages/NotFound";
import RCP from "./pages/RCP";
import Ictus from "./pages/Ictus";
import Shock from "./pages/Shock";
import ViaAerea from "./pages/ViaAerea";
import Favoritos from "./pages/Favoritos";
import Historial from "./pages/Historial";
import Ajustes from "./pages/Ajustes";
import Acerca from "./pages/Acerca";
import GaleriaImagenes from "./pages/GaleriaImagenes";
import ErrorBoundary from "@/components/ErrorBoundary"; import ErrorBoundary from "@/components/ErrorBoundary";
import PageLoader from "@/components/ui/PageLoader";
// Página principal - cargar inmediatamente (crítica)
import Home from "./pages/Index";
import NotFound from "./pages/NotFound";
// Lazy loading de páginas de contenido (cargar bajo demanda)
const SoporteVital = lazy(() => import("./pages/SoporteVital"));
const Patologias = lazy(() => import("./pages/Patologias"));
const Escena = lazy(() => import("./pages/Escena"));
const Farmacos = lazy(() => import("./pages/Farmacos"));
const Herramientas = lazy(() => import("./pages/Herramientas"));
const Material = lazy(() => import("./pages/Material"));
const Telefono = lazy(() => import("./pages/Telefono"));
const Comunicacion = lazy(() => import("./pages/Comunicacion"));
const ManualIndex = lazy(() => import("./pages/ManualIndex"));
const ManualViewer = lazy(() => import("./pages/ManualViewer"));
const RCP = lazy(() => import("./pages/RCP"));
const Ictus = lazy(() => import("./pages/Ictus"));
const Shock = lazy(() => import("./pages/Shock"));
const ViaAerea = lazy(() => import("./pages/ViaAerea"));
// Lazy loading de páginas de utilidades
const Favoritos = lazy(() => import("./pages/Favoritos"));
const Historial = lazy(() => import("./pages/Historial"));
const Ajustes = lazy(() => import("./pages/Ajustes"));
const Acerca = lazy(() => import("./pages/Acerca"));
const GaleriaImagenes = lazy(() => import("./pages/GaleriaImagenes"));
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@ -57,32 +64,34 @@ const App = () => {
<main className="pt-14 pb-safe flex-1"> <main className="pt-14 pb-safe flex-1">
<div className="container max-w-2xl py-4"> <div className="container max-w-2xl py-4">
<Routes> <Suspense fallback={<PageLoader />}>
<Route <Routes>
path="/" <Route
element={<Home onSearchClick={() => setIsSearchOpen(true)} />} path="/"
/> element={<Home onSearchClick={() => setIsSearchOpen(true)} />}
<Route path="/soporte-vital" element={<SoporteVital />} /> />
<Route path="/patologias" element={<Patologias />} /> <Route path="/soporte-vital" element={<SoporteVital />} />
<Route path="/escena" element={<Escena />} /> <Route path="/patologias" element={<Patologias />} />
<Route path="/farmacos" element={<Farmacos />} /> <Route path="/escena" element={<Escena />} />
<Route path="/herramientas" element={<Herramientas />} /> <Route path="/farmacos" element={<Farmacos />} />
<Route path="/material" element={<Material />} /> <Route path="/herramientas" element={<Herramientas />} />
<Route path="/telefono" element={<Telefono />} /> <Route path="/material" element={<Material />} />
<Route path="/comunicacion" element={<Comunicacion />} /> <Route path="/telefono" element={<Telefono />} />
<Route path="/manual" element={<ManualIndex />} /> <Route path="/comunicacion" element={<Comunicacion />} />
<Route path="/manual/:parte/:bloque/:capitulo" element={<ManualViewer />} /> <Route path="/manual" element={<ManualIndex />} />
<Route path="/rcp" element={<RCP />} /> <Route path="/manual/:parte/:bloque/:capitulo" element={<ManualViewer />} />
<Route path="/ictus" element={<Ictus />} /> <Route path="/rcp" element={<RCP />} />
<Route path="/shock" element={<Shock />} /> <Route path="/ictus" element={<Ictus />} />
<Route path="/via-aerea" element={<ViaAerea />} /> <Route path="/shock" element={<Shock />} />
<Route path="/favoritos" element={<Favoritos />} /> <Route path="/via-aerea" element={<ViaAerea />} />
<Route path="/historial" element={<Historial />} /> <Route path="/favoritos" element={<Favoritos />} />
<Route path="/ajustes" element={<Ajustes />} /> <Route path="/historial" element={<Historial />} />
<Route path="/acerca" element={<Acerca />} /> <Route path="/ajustes" element={<Ajustes />} />
<Route path="/galeria" element={<GaleriaImagenes />} /> <Route path="/acerca" element={<Acerca />} />
<Route path="*" element={<NotFound />} /> <Route path="/galeria" element={<GaleriaImagenes />} />
</Routes> <Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
</div> </div>
</main> </main>

View file

@ -0,0 +1,12 @@
import { Loader2 } from 'lucide-react';
const PageLoader = () => {
return (
<div className="flex flex-col items-center justify-center min-h-[60vh] space-y-4">
<Loader2 className="w-8 h-8 animate-spin text-primary" />
<p className="text-muted-foreground">Cargando...</p>
</div>
);
};
export default PageLoader;

View file

@ -44,8 +44,83 @@ export default defineConfig({
// Configuración de build para incluir archivos .md e imágenes // Configuración de build para incluir archivos .md e imágenes
build: { build: {
rollupOptions: { rollupOptions: {
// Asegurar que los archivos .md e imágenes se copien al build // Code splitting: dividir el bundle en chunks más pequeños
output: { output: {
manualChunks: (id) => {
// Separar node_modules en chunks por librería
if (id.includes('node_modules')) {
// React y React DOM juntos (crítico, cargar primero)
if (id.includes('react') || id.includes('react-dom')) {
return 'vendor-react';
}
// React Router (crítico para navegación)
if (id.includes('react-router')) {
return 'vendor-router';
}
// Markdown y procesamiento de texto (grande, separar)
if (id.includes('react-markdown') || id.includes('remark') || id.includes('rehype') || id.includes('unified') || id.includes('micromark') || id.includes('mdast')) {
return 'vendor-markdown';
}
// Radix UI (componentes UI, agrupar)
if (id.includes('@radix-ui')) {
return 'vendor-ui';
}
// TanStack Query
if (id.includes('@tanstack')) {
return 'vendor-query';
}
// Icons (lucide-react)
if (id.includes('lucide-react')) {
return 'vendor-icons';
}
// Charts (recharts, si se usa)
if (id.includes('recharts')) {
return 'vendor-charts';
}
// Formularios (react-hook-form, zod)
if (id.includes('react-hook-form') || id.includes('zod') || id.includes('@hookform')) {
return 'vendor-forms';
}
// Date/time (date-fns, react-day-picker)
if (id.includes('date-fns') || id.includes('react-day-picker')) {
return 'vendor-dates';
}
// Carousel (embla)
if (id.includes('embla')) {
return 'vendor-carousel';
}
// Themes (next-themes)
if (id.includes('next-themes')) {
return 'vendor-themes';
}
// Sonner (toasts)
if (id.includes('sonner')) {
return 'vendor-toasts';
}
// Resto de node_modules pequeños
return 'vendor-other';
}
// Separar páginas en chunks individuales
if (id.includes('/src/pages/')) {
const pageName = id.split('/src/pages/')[1]?.split('.')[0];
if (pageName) {
// ManualViewer es muy grande, mantenerlo separado
if (pageName === 'ManualViewer') {
return 'page-manual-viewer';
}
return `page-${pageName.toLowerCase()}`;
}
}
// Separar componentes grandes
if (id.includes('/src/components/')) {
// MarkdownViewer es grande (usa react-markdown)
if (id.includes('MarkdownViewer')) {
return 'component-markdown';
}
}
},
assetFileNames: (assetInfo) => { assetFileNames: (assetInfo) => {
const name = assetInfo.name || ''; const name = assetInfo.name || '';