Cómo usar @defer y Defer Views en Angular 17

El rendimiento es uno de los temas más importantes cuando construimos aplicaciones. Hoy en día trabajamos con una gran cantidad de componentes y siempre debemos pensar en cómo optimizar el tamaño del bundle en nuestras apps.
El equipo de Angular lo sabe muy bien, por eso introdujo en Angular 17 una funcionalidad genial para lazy loading llamada defer views.
¿Qué son las Defer Views?
Las defer views representan la forma más sencilla de implementar lazy loading y dividir nuestro código en chunks para mejorar el rendimiento de los usuarios, descargando el código únicamente cuando de verdad lo necesitamos.
El bloque @defer nos permite cargar componentes con mucho menos código y sin recurrir a programación imperativa. Esto facilita el lazy loading y nos permite activar la carga de componentes usando disparadores predeterminados de Angular o condiciones personalizadas según las necesidades del negocio.
Pero espera un momento, ¿acaso no teníamos ya lazy loading en Angular 😠?
Sí, ya contamos con una excelente forma de usar lazy loading a nivel de rutas para generar chunks específicos. Por ejemplo, cuando el usuario navega a una ruta en particular, cargamos ese componente:
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
loadComponent: () =>
import('./pages/home/home.component').then((h) => h.HomeComponent),
},
{
path: 'products',
loadComponent: () =>
import('./pages/products/products.component').then(
(p) => p.ProductsComponent
),
},
];Sin embargo, cuando queremos cargar un componente de forma dinámica basándonos en otros eventos de la interfaz, nos vemos obligados a escribir código imperativo, combinando varias APIs y manejando múltiples estados manualmente, lo cual no siempre es sencillo.
Hoy vamos a aprender por qué deberíamos usar @defer en Angular 17 para reducir el tamaño del bundle y cargar componentes dinámicos de forma simple.
El escenario
Supongamos que creamos una landing page donde mostramos una carta con las mejores novedades de Angular 17 y al Kendo Ninja. Nuestra aplicación cuenta con los siguientes componentes:
- Letter: Muestra una lista de enlaces con las nuevas características de Angular.
- Ninja: Muestra al Ninja de Kendo UI.
Cuando el usuario marca una casilla de verificación, aparece el NinjaComponent. Al usar @if, cambiamos el booleano accepted a true y se renderiza el componente <ninja/>.
El template se ve de la siguiente manera:
<letter/>
<p>Do you accept?</p>
<input (change)="accept()" type="checkbox">
@if(accepted) {
<ninja/>
}Vamos a clonar el repositorio e instalar las dependencias para revisar el código actual:
git clone https://github.com/danywalls/learn-defer-views-angular.git
cd learn-defer-views-angular
npm iAl compilar, podemos notar un detalle importante en la salida de la terminal: un único archivo main.js, junto con polyfills.js y styles.css.

Luego, ejecuta ng serve -o para ver la aplicación en http://localhost:4200. Abre las herramientas de desarrollo del navegador y revisa la pestaña Network. Verás varios archivos, pero uno es el más relevante: main.js, el cual contiene todo el bundle de nuestra aplicación con todos los componentes empaquetados en un solo archivo.

Quizás te estés preguntando: ¿por qué le enviamos al usuario componentes que tal vez nunca llegue a ver o que solo necesita en situaciones muy puntuales?
Aquí es donde habitualmente nos tocaba implementar lazy loading manual para cargar nuestro componente en un archivo .js (o chunk) separado. Veamos cómo se hacía.
Lazy loading manual 😞
Abre app.component.ts, declara una nueva variable pública ninja de tipo Type<any>, y cambia la firma del método accept() para que retorne una promesa.
Dentro del método accept(), usando importación dinámica (dynamic import), asigna a la variable el resultado de importar NinjaComponent:
async accept(): Promise<void> {
const { NinjaComponent } = await import('./components/ninja.component');
this.ninja = NinjaComponent;
}El código final en app.component.ts queda de esta forma:
import {Component, Type} from '@angular/core';
import { RouterOutlet } from '@angular/router';
import {NgComponentOutlet} from "@angular/common";
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, NgComponentOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
public ninja!: Type<any>;
async accept(): Promise<void> {
const { NinjaComponent } = await import('./components/ninja.component');
this.ninja = NinjaComponent;
}
}Ahora en app.component.html, agrega un ng-container con la directiva ngComponentOutlet y pásale la variable ninja.
Recuerda importar NgComponentOutlet, guarda los cambios y revisa los detalles en la terminal:

¡Obtuvimos un nuevo chunk, chunk-XQI5NWYI.js, exclusivo para NinjaComponent!
Guarda los cambios y recarga la página en el navegador:

Cuando marcamos el checkbox, el chunk se descarga y el componente se renderiza. Es una optimización real porque el archivo solo viaja por la red cuando el usuario hace clic.
Pero, ¿crees que esta solución escala bien a futuro? ¿Qué pasa si mañana queremos contemplar otros escenarios de carga como estos?
- Cargar cuando el usuario hace scroll.
- Cargar cuando el navegador esté inactivo (idle).
- Mostrar un indicador visual de carga mientras se descarga.
Hacer todo eso con código imperativo resulta tedioso y difícil de mantener. Aquí es donde debemos dar el salto hacia @defer.
El bloque @defer
A partir de Angular 17 contamos con el bloque de control @defer. Nos permite escribir código declarativo para implementar lazy loading de forma limpia, mostrando el componente en cuanto se cumpla una condición o disparador y sus dependencias estén listas.
El bloque @defer se puede combinar con sub-bloques como @error, @placeholder, @loading y disparadores personalizados.
Antes de continuar, vamos a refactorizar nuestro código:
- Agrega
NinjaComponentdirectamente al arreglo deimportsdel componente. - Declara la variable
acceptedinicializada enfalse. - Cambia el método
accept()para que retornevoidy simplemente alterne el valor deaccepted.
El código final se ve así:
import {Component} from '@angular/core';
import { RouterOutlet } from '@angular/router';
import {NgComponentOutlet} from "@angular/common";
import {NinjaComponent} from "./components/ninja.component";
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, NgComponentOutlet, NinjaComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
accepted = false;
public accept(): void {
this.accepted = !this.accepted;
}
}En la plantilla HTML, actualizamos para usar el bloque @defer. Por defecto, Angular carga el componente cuando el navegador entra en estado de inactividad (idle), pero también podemos combinarlo con disparadores mediante when y on.
Por ejemplo, el bloque @defer se renderizará con when en cuanto la variable accepted sea verdadera:
@defer (when accepted) {
<ninja/>
}Guarda los cambios. La salida del compilador sigue dividiendo el código en chunks, pero eliminamos todo el código repetitivo del import() dinámico y NgComponentOutlet:

Todo sigue funcionando con lazy loading: el componente ninja solo se descarga cuando el usuario hace clic.

Además de condiciones con when, también tenemos el disparador on, que nos permite activar el bloque a partir de eventos específicos:
on viewport
El bloque diferido se activa cuando el elemento entra en el área visible (viewport), utilizando la API IntersectionObserver.
@defer (on viewport) {
<ninja/>
} @placeholder {
<div class="ninja-placeholder"> </div>
}on interaction
El bloque diferido se activa cuando el usuario interactúa con el elemento disparador (por ejemplo, al hacer clic o recibir el foco).
@defer (on interaction) {
<ninja/>
} @placeholder {
<div class="ninja-placeholder"> </div>
}on hover
El bloque diferido se activa cuando el cursor pasa por encima del área del disparador.
@defer (on hover) {
<ninja/>
} @placeholder {
<div class="ninja-placeholder"> </div>
}on timer
El bloque diferido se activa después de un tiempo determinado.
@defer (on timer(4s)) {
<ninja/>
} @placeholder {
<div class="ninja-placeholder"> </div>
}Podríamos seguir mostrando más ejemplos de disparadores, pero te recomiendo leer más en la documentación oficial sobre triggers y avanzar al uso de los bloques @placeholder y @loading 😊.
Los bloques @placeholder y @loading
Vamos a aplicar lazy loading al componente Ninja cuando el usuario haga clic para mostrarlo, pero con un detalle adicional: queremos mostrar un placeholder en el espacio donde aparecerá el Ninja. Esto nos ayuda a evitar saltos visuales en la página (layout shifts), reservando ese espacio con anticipación.
Mostremos el texto "¿Estás listo?" en el área donde aparecerá NinjaComponent:
@defer (when accepted) {
<ninja/>
} @placeholder {
<p>Are you ready ?</p>
}Guarda los cambios y recarga la página. Verás el mensaje en el lugar exacto donde se renderizará el componente:

¡Excelente, funciona! Ahora mejoremos la experiencia de carga. Por ejemplo, además del placeholder, quiero mostrar un mensaje mientras el componente se está descargando. Para lograrlo, combino el bloque @loading con el parámetro minimum configurado en 5 segundos:
@loading (minimum 5s) {
<h2>The Ninja is coming...</h2>
}
Guarda los cambios y pruébalo otra vez. Todo funciona exactamente como esperábamos. Recuerda que @placeholder y @loading son bloques opcionales, pero aportan muchísimo valor para mejorar la experiencia de usuario.
Importante: El código dentro de
@placeholdery@loadingse incluye en el bundle principal (main.js).
Conclusión
Revisamos las ventajas de usar la funcionalidad @defer en Angular 17 para aplicar lazy loading a componentes de forma dinámica, lo cual mejora el rendimiento de nuestras aplicaciones al descargar código solo cuando realmente se necesita. El bloque @defer simplifica todo el flujo al eliminar la necesidad de escribir código imperativo manual.
También vimos cómo utilizar diferentes disparadores como la entrada en el viewport, la interacción del usuario y temporizadores. Además, mejoramos la experiencia de usuario con los bloques @placeholder y @loading para gestionar la interfaz mientras se descargan las dependencias.
- Código inicial: https://github.com/danywalls/learn-defer-views-angular
- Importaciones dinámicas: https://github.com/danywalls/learn-defer-views-angular/tree/dynamic-imports
- Defer views: https://github.com/danywalls/learn-defer-views-angular/tree/feature/defer-views
Modern frontend development — Angular, React, TypeScript, accessibility, and performance.
Preguntas Frecuentes
¿Qué es el bloque @defer en Angular 17 y cómo mejora el rendimiento de la aplicación?
El bloque @defer es una sintaxis declarativa en Angular 17 que permite aplicar lazy loading a componentes y sus dependencias directamente desde el template, sin necesidad de escribir JavaScript imperativo. Al envolver un componente con @defer, Angular lo separa en un chunk de JavaScript independiente que solo se descarga y renderiza cuando se cumple una condición o disparador específico, reduciendo el tamaño del bundle inicial y acelerando la carga de la página.
¿Qué disparadores puedo usar con el bloque @defer en Angular 17 para controlar cuándo se carga un componente?
Angular 17 ofrece varios disparadores integrados para el bloque @defer, incluyendo on viewport (se carga cuando el elemento entra en el área visible), on interaction (se carga al hacer clic o foco), on idle (se carga cuando el navegador está inactivo), on timer (se carga tras un tiempo de espera) y on hover. También puedes combinar varios disparadores y usar una condición personalizada con when mediante una expresión booleana.
¿En qué se diferencia @defer del lazy loading con loadComponent en el router de Angular?
El lazy loading basado en rutas con loadComponent divide el código a nivel de enrutamiento y carga un componente cuando el usuario navega a una URL específica. Por otro lado, el bloque @defer funciona a nivel de template y ofrece un control mucho más granular dentro de una misma vista, permitiéndote diferir cualquier componente según la interacción del usuario, la posición del scroll o la inactividad del navegador, sin depender de eventos de navegación.
Artículos Relacionados
Angular y Domino: Una combinación potente para Server-Side Rendering.
Si estás usando Angular y necesitas acceder al DOM con SSR, usa el paquete domino. Domino ofrece soporte para la API del DOM y selectores CSS para querySelector(), querySelectorAll() y matches(). Instala el paquete domino desde tu terminal.
La propiedad 'exportAs' en Angular: Guía práctica para compartir el estado de componentes
En Angular, cuando creamos componentes con propiedades públicas o estado para compartir con otros, existen varias opciones: emitir eventos, inyectar un servicio con Subject o usar la propiedad exportAs en el template.
Compartir este artículo
Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.
Software real. Lecciones reales.
Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.
Únete a 13,800+ desarrolladores y lectores.
Cero spam. Cancela tu suscripción en cualquier momento.