Lección 2 de 411 min
Enviar menos código
Lazy loading, code splitting y tree shaking: que el navegador descargue solo lo que necesita.
Tu tienda envía dos megas de JavaScript en la primera carga. En un móvil de gama media, descargarlos ya cuesta, pero lo peor viene después: el navegador tiene que analizar y ejecutar todo ese código antes de que la página responda, y eso se nota en el LCP y, sobre todo, en el INP. Además, una buena parte es el panel de administración, que solo usan tres empleados.
El JavaScript más rápido es el que no se envía. En esta lección verás cómo retrasar lo que no hace falta todavía (lazy loading), cómo trocear la aplicación en partes (code splitting), cómo eliminar lo que nadie usa (tree shaking) y cómo ponerte límites para que el problema no vuelva (presupuestos de rendimiento).
Cargar solo cuando haga falta
La primera vista de una página casi nunca necesita todo: las fotos del final, un mapa incrustado o el editor de un formulario que nadie ha abierto pueden esperar.
Lazy loading de imágenes e iframes
El navegador descarga las imágenes y los iframes solo cuando se acercan a la pantalla.
- Atributo:
- loading="lazy"
- Para:
- img e iframe
Lazy loading (carga diferida) es retrasar la descarga de todo lo que no se necesita para la primera vista hasta el momento en que hace falta. Para imágenes e iframes, los navegadores lo hacen de forma nativa con el atributo loading="lazy": no los piden hasta que el usuario se acerca a ellos con el scroll.
Así la red queda libre para lo importante al principio, y quien no baja hasta el final no descarga lo que nunca iba a ver. Para el código, los equivalentes son el import() dinámico y, en Angular, @defer y las rutas lazy, que verás a continuación.
Úsalo cuando
En las imágenes y los iframes que quedan por debajo de lo que se ve al cargar: galerías, fotos de opiniones, mapas, vídeos incrustados.
Ten en cuenta
Nunca en la imagen principal ni en nada visible al cargar: retrasarías el LCP. Y pon siempre width y height, o el contenido saltará cuando aparezcan.
<!-- Fotos de opiniones al final: se piden al acercarse con el scroll -->
<img src="opinion-marta.jpg" alt="Foto de Marta con las zapatillas"
width="400" height="400" loading="lazy">
<!-- Un mapa que pesa mucho y que casi nadie mira -->
<iframe src="https://mapas.ejemplo.com/tienda" title="Cómo llegar"
width="600" height="400" loading="lazy"></iframe>import() dinámico
Cargar un módulo de JavaScript en el momento en que se necesita, no al arrancar.
- Devuelve:
- Una promesa
- Estándar:
- ES2020
Un import normal, arriba del archivo, mete el módulo en el mismo paquete que el código que lo usa. El import() dinámico es una función que devuelve una promesa y descarga el módulo cuando la llamas. Los bundlers lo detectan y ponen ese módulo en un archivo aparte.
Es perfecto para librerías pesadas que solo usan algunas personas: exportar a PDF o a Excel, un editor de texto enriquecido, una librería de gráficos para un informe que se abre de vez en cuando.
Úsalo cuando
Cuando una función de la aplicación depende de código pesado y no se usa nada más cargar la página.
Ten en cuenta
El primer clic tiene que esperar a la descarga: muestra un estado de carga, o adelanta la descarga cuando el usuario pasa el ratón por encima o el navegador está libre. Y escribe la ruta como texto fijo, para que el bundler sepa qué archivo separar.
const boton = document.querySelector("#exportar-pdf");
boton.addEventListener("click", async () => {
boton.textContent = "Preparando…";
// La librería de PDF solo se descarga si alguien pulsa el botón
const { generarPdf } = await import("./exportar-pdf.js");
await generarPdf(pedido);
boton.textContent = "Exportar a PDF";
});Angular: rutas lazy y @defer
Separar páginas enteras y trozos de plantilla en archivos que se cargan cuando tocan.
- Disparador por defecto:
- on idle
- Rutas:
- loadComponent, loadChildren
Las rutas lazy cargan una página solo cuando el usuario navega a ella: en la configuración del router usas loadComponent con un import() dinámico (o loadChildren para un grupo de rutas), y Angular pone ese código en su propio archivo.
Los bloques @defer hacen lo mismo dentro de una plantilla. Envuelves un componente pesado y Angular lo separa, junto con sus dependencias, y lo carga según un disparador: on viewport al entrar en pantalla, on interaction al pulsar, on hover, on timer(…), when una condición o, si no indicas ninguno, on idle, cuando el navegador está libre. Con @placeholder y @loading decides qué se ve mientras tanto. Lo viste también en el módulo de frameworks, al hablar de rendimiento.
Úsalo cuando
Rutas lazy para cualquier sección que no sea la de entrada, como el área de cliente o la administración; @defer para widgets pesados por debajo de lo visible, como comentarios, mapas o gráficos.
Ten en cuenta
Los componentes dentro de @defer deben ser standalone y no usarse fuera del bloque en esa misma plantilla, o Angular los cargará desde el principio. Y no difieras lo que se ve al cargar: el usuario vería el placeholder y luego un salto.
// app.routes.ts: la administración, en su propio archivo
export const routes: Routes = [
{ path: "", component: Portada },
{ path: "admin", loadComponent: () => import("./admin/panel").then((m) => m.Panel) },
];
<!-- producto.html: las opiniones, al acercarse con el scroll -->
@defer (on viewport; prefetch on idle) {
<app-opiniones [productoId]="producto().id" />
} @placeholder {
<p>Opiniones de clientes</p>
}Trocear y podar el paquete
Estas dos técnicas las aplica el bundler al compilar, como viste en el módulo de herramientas de build. Tu trabajo es escribir el código de forma que pueda aplicarlas.
Code splitting
Dividir la aplicación en trozos (chunks) para que cada página descargue solo lo suyo.
- Puntos de corte:
- import()
- Resultado:
- Chunks
En lugar de un único main.js enorme, el bundler genera varios chunks: el inicial, con lo que necesita cualquier página, y otros que se descargan bajo demanda. Los puntos de corte son tus import() dinámicos: por ruta, un chunk por página, y por componente, para un widget pesado.
El bundler también extrae los chunks compartidos: si dos páginas usan la misma librería, va en un archivo aparte que se descarga una sola vez y queda en caché. Y como cada chunk lleva un hash en el nombre, un cambio en una página no invalida la caché de las demás.
Úsalo cuando
En cualquier aplicación con varias páginas o secciones. Empieza por las rutas: es el corte que más ahorra con menos esfuerzo.
Ten en cuenta
Demasiados chunks pequeños crean cascadas de peticiones: uno descarga, descubre que necesita otro, y así sucesivamente. Lo que más importa es el tamaño del chunk inicial, porque es el que retrasa el LCP y el INP.
Initial chunk files | Names | Raw size
main-7KQ2XG3B.js | main | 182.40 kB
styles-5INURTSO.css | styles | 12.10 kB
Lazy chunk files | Names | Raw size
chunk-ADM4F2QX.js | panel | 245.80 kB
chunk-R5LCS7PE.js | opiniones | 38.20 kBTree shaking
Quitar del paquete el código que se exporta pero que nadie importa.
- Requiere:
- Módulos ES
- Lo frena:
- CommonJS
El tree shaking (sacudir el árbol para que caigan las hojas secas) elimina las funciones y módulos que nadie usa. Funciona gracias a que import y export de los módulos ES son estáticos: el bundler sabe al compilar exactamente qué se usa y qué no.
Con CommonJS no puede: require() es una llamada a una función que se puede hacer con un nombre calculado o dentro de un if, así que el bundler no se atreve a quitar nada. Por eso lodash, en CommonJS, entra entero, y lodash-es solo con lo que usas.
Las librerías pueden ayudar con "sideEffects": false en su package.json: le dice al bundler que importar un módulo no hace nada más que exportar cosas, así que puede descartar entero cualquier módulo que no se use. Si algunos archivos sí tienen efectos, como el CSS, se listan: "sideEffects": ["*.css"].
Úsalo cuando
Siempre: elige librerías que publiquen módulos ES e importa solo lo que necesitas por su nombre.
Ten en cuenta
Si una librería marca como libre de efectos un archivo que sí los tiene, como un polyfill o un CSS global, el bundler lo eliminará y algo dejará de funcionar sin dar error al compilar.
// Mal: CommonJS, el bundler incluye la librería entera
const _ = require("lodash");
const buscarConRetraso = _.debounce(buscar, 300);
// Bien: módulos ES, solo entra debounce y lo que necesita
import { debounce } from "lodash-es";
const buscarConRetraso = debounce(buscar, 300);Pruébalo
Abre el panel Coverage de DevTools en tu aplicación y recarga la portada. Si más de la mitad del JavaScript aparece en rojo, sin usar, tienes candidatos claros para code splitting.
Ponerse límites
Presupuestos de rendimiento
Un límite de tamaño o de tiempo que la build vigila y que no puedes superar sin enterarte.
- En Angular:
- budgets
- Si se supera el error:
- Falla la build
Un presupuesto de rendimiento es un compromiso medible: «el JavaScript inicial no pasa de 300 kB», «el LCP de laboratorio, menos de 2,5 segundos», «menos de 50 peticiones». Sin él, cada nueva función añade un poco y nadie se da cuenta hasta que la web ya va lenta.
Angular trae presupuestos de tamaño en angular.json, dentro de budgets. Cada uno tiene un tipo, como initial (el paquete inicial) o anyComponentStyle (el CSS de cada componente), y dos umbrales: al superar maximumWarning avisa, y al superar maximumError, la build falla. Para métricas como el LCP, Lighthouse CI permite fijar límites similares en el pipeline.
Úsalo cuando
Desde el inicio del proyecto, o en cuanto midas el estado actual. Es la única forma de que el rendimiento no empeore poco a poco.
Ten en cuenta
Parte de lo que mides hoy y ajústalo poco a poco: un presupuesto imposible acaba subiéndose sin pensar. Y recuerda que 100 kB de JavaScript cuestan más que 100 kB de imagen, porque además hay que analizarlos y ejecutarlos.
"configurations": {
"production": {
"budgets": [
{ "type": "initial", "maximumWarning": "500kB", "maximumError": "1MB" },
{ "type": "anyComponentStyle", "maximumWarning": "4kB", "maximumError": "8kB" }
]
}
}Interactivo
Cuatro formas de enviar menos
Activa o desactiva opciones para compararlas cara a cara.
| Característica | Lazy loading | Code splitting | Tree shaking |
|---|---|---|---|
| Qué hace | Retrasa la descarga hasta que hace falta | Divide la aplicación en chunks | Elimina lo que nadie importa |
| Cuándo actúa | En el navegador, mientras se usa la página | Al compilar | Al compilar |
| Quién lo hace | Tú, con loading, import() o @defer | El bundler, a partir de tus import() | El bundler, si el código usa módulos ES |
| Ejemplo típico | Fotos del final, un editor que casi nadie abre | Un chunk por ruta | Solo las funciones de lodash-es que usas |
Ponlo en práctica
Saber qué es cada cosa es la mitad del trabajo. La otra mitad es reconocer cuándo usarla.
Interactivo
¿Qué usarías?
Lee cada situación y elige la opción que mejor la resuelve. Fallar aquí no cuenta: es para practicar.
La ficha de producto muestra al final treinta fotos de opiniones de clientes, y casi nadie baja hasta ahí.
Para recordar
- El JavaScript más rápido es el que no se envía: además de descargarse, hay que analizarlo y ejecutarlo.
loading="lazy"retrasa imágenes e iframes;import(),@defery las rutas lazy retrasan el código. Nunca difieras lo que se ve al cargar.- El code splitting divide la aplicación en chunks por ruta y por componente; el chunk inicial es el que más importa.
- El tree shaking necesita módulos ES: CommonJS lo impide, y
sideEffectsayuda al bundler a descartar módulos enteros. - Un presupuesto de rendimiento, como los
budgetsde Angular, evita que el tamaño crezca sin que nadie lo note.
Comprueba lo aprendido
Cuestionario
Responde las 5 preguntas para completar la lección.
1.¿Por qué CommonJS dificulta el tree shaking?
2.¿Qué ocurre si pones
loading="lazy"en la imagen principal de la portada?3.En Angular escribes
@defer { <app-grafico /> }sin indicar ningún disparador. ¿Cuándo se carga?4.¿Qué le dice
"sideEffects": falseen elpackage.jsonde una librería al bundler?5.En
angular.jsontienes un presupuestoinitialconmaximumError: "1MB", y el paquete inicial ocupa 1,2 MB. ¿Qué pasa?