Angular 20: Revolución en Reactividad y Performance - Todas las Novedades
Angular 20 marca un hito transformador en el desarrollo web moderno, consolidando años de innovación en reactividad con Signals y abriendo las puertas a aplicaciones Zoneless de alto rendimiento. Esta versión major, lanzada en mayo de 2025, representa uno de los avances más significativos del framework en años.
Lo Más Destacado de Angular 20
🚀 Principales Novedades
- Signals API Estabilizado: Reactividad nativa sin dependencias externas
- Arquitectura Zoneless: Developer Preview con mejoras de performance
- Control Flow Nativo: Deprecación oficial de directivas estructurales
- Testing Modernizado: Soporte experimental para Vitest
- TypeScript 5.8+: Requisito mínimo actualizado
- SSR Mejorado: Mejores error handlers y integración
Signals: La Nueva Era de la Reactividad
Estabilización Completa
Angular 20 estabiliza completamente la API de Signals, proporcionando una base sólida para la reactividad basada en señales. Esto significa que los Signals son ahora production-ready y no sufrirán cambios breaking en versiones futuras.
// Signals estabilizados en Angular 20
import { signal, computed, effect } from "@angular/core";
@Component({
selector: "app-counter",
template: `
<div>
<p>Count: {{ count() }}</p>
<p>Double: {{ doubleCount() }}</p>
<button (click)="increment()">+</button>
<button (click)="decrement()">-</button>
</div>
`,
})
export class CounterComponent {
// Signal básico
count = signal(0);
// Computed signal
doubleCount = computed(() => this.count() * 2);
constructor() {
// Effect para side effects
effect(() => {
console.log(`Count changed to: ${this.count()}`);
});
}
increment() {
this.count.update((current) => current + 1);
}
decrement() {
this.count.update((current) => current - 1);
}
}
Integración con RxJS Mejorada
// Nueva integración Signals + RxJS
import { toSignal, toObservable } from "@angular/core/rxjs-interop";
import { HttpClient } from "@angular/common/http";
@Injectable()
export class DataService {
private http = inject(HttpClient);
// Signal desde Observable
users = toSignal(this.http.get<User[]>("/api/users"), { initialValue: [] });
// Observable desde Signal
selectedUserId = signal<number | null>(null);
selectedUser$ = toObservable(this.selectedUserId);
}
Signals en Forms
// Reactive Forms con Signals
import { FormControl, FormGroup } from "@angular/forms";
import { toSignal } from "@angular/core/rxjs-interop";
@Component({
selector: "app-form",
template: `
<form [formGroup]="userForm">
<input formControlName="name" placeholder="Name" />
<input formControlName="email" placeholder="Email" />
<!-- Reactive values -->
<p>Form Valid: {{ isValid() }}</p>
<p>Form Value: {{ formValue() | json }}</p>
</form>
`,
})
export class FormComponent {
userForm = new FormGroup({
name: new FormControl(""),
email: new FormControl(""),
});
// Signals desde form state
isValid = toSignal(this.userForm.statusChanges, { initialValue: false });
formValue = toSignal(this.userForm.valueChanges, { initialValue: {} });
}
Arquitectura Zoneless: El Futuro del Performance
¿Qué es Zoneless?
Angular 20 introduce Zoneless en Developer Preview, eliminando la dependencia de Zone.js para mejorar significativamente el rendimiento. Esto resulta en:
- Bundles más pequeños: Reducción del tamaño final
- Rendering más rápido: Menos overhead en detección de cambios
- Mejor performance: Especialmente en aplicaciones complejas
Habilitando Zoneless
// main.ts - Configuración Zoneless
import { bootstrapApplication } from "@angular/platform-browser";
import { provideExperimentalZonelessChangeDetection } from "@angular/core";
bootstrapApplication(AppComponent, {
providers: [
// Habilitar arquitectura Zoneless
provideExperimentalZonelessChangeDetection(),
// ... otros providers
],
});
Componentes Zoneless
// Componente optimizado para Zoneless
@Component({
selector: "app-zoneless-example",
template: `
<div>
<h2>{{ title() }}</h2>
<p>Count: {{ count() }}</p>
<button (click)="increment()">Increment</button>
</div>
`,
// Estrategia de cambio explícita para Zoneless
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZonelessComponent {
title = signal("Zoneless Component");
count = signal(0);
increment() {
// Los signals activan cambios automáticamente en Zoneless
this.count.update((c) => c + 1);
}
}
Migración a Zoneless
// Antes: Dependiente de Zone.js
@Component({
template: `<div>{{ data }}</div>`,
})
export class OldComponent {
data = "Hello";
ngOnInit() {
// Esto requiere Zone.js para detectar cambios
setTimeout(() => {
this.data = "Updated";
}, 1000);
}
}
// Después: Optimizado para Zoneless
@Component({
template: `<div>{{ data() }}</div>`,
})
export class NewComponent {
data = signal("Hello");
ngOnInit() {
// Los signals manejan la reactividad automáticamente
setTimeout(() => {
this.data.set("Updated");
}, 1000);
}
}
Control Flow: Adiós a las Directivas Estructurales
Deprecación Oficial
Angular 20 depreca oficialmente las directivas estructurales *ngIf, *ngFor y *ngSwitch en favor de la nueva sintaxis de control flow.
Sintaxis Antigua vs Nueva
// ❌ DEPRECADO: Directivas estructurales
@Component({
template: `
<!-- ngIf deprecado -->
<div *ngIf="showContent">Content visible</div>
<!-- ngFor deprecado -->
<div *ngFor="let item of items; let i = index">
{{ i }}: {{ item.name }}
</div>
<!-- ngSwitch deprecado -->
<div [ngSwitch]="status">
<p *ngSwitchCase="'loading'">Loading...</p>
<p *ngSwitchCase="'error'">Error occurred</p>
<p *ngSwitchDefault>Content loaded</p>
</div>
`,
})
export class OldSyntaxComponent {
showContent = true;
items = [{ name: "Item 1" }, { name: "Item 2" }];
status = "loading";
}
// ✅ NUEVO: Control Flow nativo
@Component({
template: `
<!-- Nueva sintaxis @if -->
@if (showContent()) {
<div>Content visible</div>
}
<!-- Nueva sintaxis @for -->
@for (item of items(); track item.id; let i = $index) {
<div>{{ i }}: {{ item.name }}</div>
}
<!-- Nueva sintaxis @switch -->
@switch (status()) { @case ('loading') {
<p>Loading...</p>
} @case ('error') {
<p>Error occurred</p>
} @default {
<p>Content loaded</p>
} }
`,
})
export class NewSyntaxComponent {
showContent = signal(true);
items = signal([
{ id: 1, name: "Item 1" },
{ id: 2, name: "Item 2" },
]);
status = signal<"loading" | "error" | "success">("loading");
}
Migración Automática
# Comando para migrar automáticamente
ng generate @angular/core:control-flow
# Migra automáticamente:
# *ngIf → @if
# *ngFor → @for
# *ngSwitch → @switch
Ventajas del Nuevo Control Flow
// Mejor performance y type safety
@Component({
template: `
<!-- Type safety mejorado -->
@if (user(); as u) {
<div>Welcome {{ u.name }}!</div>
}
<!-- Track functions optimizadas -->
@for (product of products(); track product.id) {
<product-card [product]="product" />
}
<!-- Sintaxis más limpia -->
@switch (userRole()) { @case ('admin') {
<admin-panel />
} @case ('user') {
<user-dashboard />
} @default {
<login-form />
} }
`,
})
export class OptimizedComponent {
user = signal<User | null>(null);
products = signal<Product[]>([]);
userRole = signal<"admin" | "user" | "guest">("guest");
}
Testing: Modernización con Vitest
Adiós a Karma
Con Karma oficialmente deprecado, Angular 20 introduce soporte experimental para Vitest como alternativa moderna de testing.
Configuración de Vitest
// vite.config.ts
import { defineConfig } from "vite";
import { angular } from "@angular-devkit/build-angular/plugins/vite";
export default defineConfig({
plugins: [angular()],
test: {
globals: true,
environment: "jsdom",
setupFiles: ["src/test-setup.ts"],
include: ["src/**/*.{test,spec}.{js,ts}"],
},
});
Tests con Vitest y Signals
// user.component.spec.ts
import { describe, it, expect } from "vitest";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { signal } from "@angular/core";
describe("UserComponent", () => {
let component: UserComponent;
let fixture: ComponentFixture<UserComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserComponent],
}).compileComponents();
fixture = TestBed.createComponent(UserComponent);
component = fixture.componentInstance;
});
it("should update user signal", () => {
const newUser = { id: 1, name: "John Doe" };
component.user.set(newUser);
expect(component.user()).toEqual(newUser);
});
it("should compute full name correctly", () => {
component.firstName.set("John");
component.lastName.set("Doe");
expect(component.fullName()).toBe("John Doe");
});
});
Testing Zoneless Applications
// zoneless-component.spec.ts
import { provideExperimentalZonelessChangeDetection } from "@angular/core";
describe("ZonelessComponent", () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ZonelessComponent],
providers: [provideExperimentalZonelessChangeDetection()],
}).compileComponents();
});
it("should work with zoneless change detection", () => {
const fixture = TestBed.createComponent(ZonelessComponent);
const component = fixture.componentInstance;
component.count.set(5);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain("5");
});
});
Server-Side Rendering Mejorado
Error Handling Avanzado
Angular 20 introduce error handlers por defecto para SSR, manejando unhandledRejection y uncaughtException.
// app.config.server.ts
import { ApplicationConfig } from "@angular/core";
import { provideServerRendering } from "@angular/platform-server";
export const serverConfig: ApplicationConfig = {
providers: [
provideServerRendering({
// Nuevos error handlers
enableDefaultErrorHandlers: true,
errorHandlers: {
unhandledRejection: (reason, promise) => {
console.error("Unhandled Rejection:", reason);
},
uncaughtException: (error, origin) => {
console.error("Uncaught Exception:", error, origin);
},
},
}),
],
};
SSR con Signals
// Componente optimizado para SSR
@Component({
selector: "app-ssr-example",
template: `
<div>
<h1>{{ title() }}</h1>
@if (data(); as d) {
<pre>{{ d | json }}</pre>
} @else {
<p>Loading...</p>
}
</div>
`,
})
export class SSRComponent {
private http = inject(HttpClient);
title = signal("SSR with Signals");
data = signal<any>(null);
ngOnInit() {
// Los signals funcionan perfectamente en SSR
this.http.get("/api/data").subscribe((result) => this.data.set(result));
}
}
Requisitos y Breaking Changes
Requisitos Mínimos
Angular 20 requiere TypeScript 5.8+ y Node.js 20+, asegurando acceso a las últimas características del lenguaje.
// package.json - Requisitos mínimos
{
"engines": {
"node": ">=20.0.0"
},
"devDependencies": {
"typescript": "^5.8.0",
"@angular/cli": "^20.0.0"
}
}
Principales Breaking Changes
1. TypeScript 5.8+ Obligatorio
// Ahora disponible: TypeScript 5.8 features
interface User {
readonly id: number;
name: string;
email?: string;
}
// Mejor inference y type safety
const users: User[] = [
{ id: 1, name: "John" },
{ id: 2, name: "Jane", email: "jane@example.com" },
];
2. Directivas Estructurales Deprecadas
# Comando de migración obligatorio para futuras versiones
ng generate @angular/core:control-flow
# Alternativas para migración manual:
# *ngIf="condition" → @if (condition) { }
# *ngFor="let item of items" → @for (item of items; track item.id) { }
3. Cambios en el Style Guide
Angular 20 incluye una actualización mayor del style guide, eliminando muchas recomendaciones para enfocarse en las más importantes.
// ❌ Nombres de archivo anteriores
user - profile.component.ts;
user - profile.service.ts;
user - profile.module.ts;
// ✅ Nueva convención simplificada
user - profile.ts(componente);
user - profile.ts(servicio);
user - profile.ts(módulo);
Guía de Migración
Paso 1: Preparación del Entorno
# Actualizar Node.js a v20+
nvm install 20
nvm use 20
# Verificar versiones
node --version # >= 20.0.0
npm --version # Actualizado automáticamente
Paso 2: Actualización de Dependencias
# Actualizar Angular CLI globalmente
npm install -g @angular/cli@20
# Actualizar proyecto
ng update @angular/core@20 @angular/cli@20
# Actualizar TypeScript
npm install typescript@^5.8.0 --save-dev
Paso 3: Migración de Control Flow
# Migración automática de directivas estructurales
ng generate @angular/core:control-flow
# Revisar y ajustar cambios generados
git diff --staged
Paso 4: Migración a Signals (Opcional)
// Migración gradual de propiedades a signals
export class ComponentMigration {
// Antes
// count = 0;
// Después
count = signal(0);
// Antes
// get doubleCount() { return this.count * 2; }
// Después
doubleCount = computed(() => this.count() * 2);
}
Paso 5: Habilitación de Zoneless (Experimental)
// main.ts
import { provideExperimentalZonelessChangeDetection } from "@angular/core";
bootstrapApplication(AppComponent, {
providers: [
provideExperimentalZonelessChangeDetection(),
// ... otros providers
],
});
Performance y Optimizaciones
Mejoras de Bundle Size
// Tree-shaking mejorado con Signals
import { signal, computed } from "@angular/core";
// Sin Zone.js, bundles más pequeños
const APP_SIZE_REDUCTION = "~15-20%"; // Aproximado
Optimizaciones de Runtime
// Change Detection más eficiente
@Component({
selector: "app-optimized",
template: `
<!-- Reactividad automática con Signals -->
<div>{{ status() }}</div>
<div>{{ computedValue() }}</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OptimizedComponent {
status = signal("ready");
value = signal(10);
// Computed values son memoizados automáticamente
computedValue = computed(() => {
console.log("Computing..."); // Solo se ejecuta cuando cambia 'value'
return this.value() * 2;
});
}
Lazy Loading Mejorado
// Lazy loading con mejor performance
const routes: Routes = [
{
path: "feature",
loadComponent: () =>
import("./feature/feature.component").then((m) => m.FeatureComponent),
},
{
path: "admin",
loadChildren: () =>
import("./admin/admin.routes").then((m) => m.adminRoutes),
},
];
Herramientas de Desarrollo
CLI Enhancements
# Nuevos comandos y opciones
ng generate component --signals # Genera componente con signals
ng generate service --signals # Genera servicio con signals
ng build --zoneless # Build optimizado para zoneless
ng test --vitest # Testing con Vitest
DevTools Mejoradas
// Mejor debugging de Signals
if (import.meta.env.DEV) {
// Debugging automático de signals en desarrollo
effect(() => {
console.log("Signal changed:", this.debugSignal());
});
}
Análisis de Bundle
# Análisis mejorado de bundles
ng build --stats-json --source-map
npx webpack-bundle-analyzer dist/stats.json
Migración de Librerías Populares
Angular Material
// Angular Material con Signals
import { MatButtonModule } from "@angular/material/button";
import { signal } from "@angular/core";
@Component({
template: `
<button mat-raised-button [disabled]="isLoading()" (click)="submit()">
{{ buttonText() }}
</button>
`,
imports: [MatButtonModule],
})
export class MaterialComponent {
isLoading = signal(false);
buttonText = computed(() => (this.isLoading() ? "Loading..." : "Submit"));
async submit() {
this.isLoading.set(true);
try {
await this.someAsyncOperation();
} finally {
this.isLoading.set(false);
}
}
}
NgRx con Signals
// Integración NgRx + Signals
@Injectable()
export class SignalStore {
private store = inject(Store);
// Signal desde store
user = toSignal(this.store.select(selectUser), { initialValue: null });
// Dispatch actions
updateUser(user: User) {
this.store.dispatch(UserActions.updateUser({ user }));
}
}
Casos de Uso y Ejemplos Reales
E-commerce con Signals
@Component({
selector: "app-shopping-cart",
template: `
<div class="cart">
<h2>Shopping Cart ({{ itemCount() }})</h2>
@for (item of cartItems(); track item.id) {
<div class="cart-item">
<span>{{ item.name }}</span>
<span>{{ item.price | currency }}</span>
<button (click)="removeItem(item.id)">Remove</button>
</div>
}
<div class="total">Total: {{ totalPrice() | currency }}</div>
</div>
`,
})
export class ShoppingCartComponent {
cartItems = signal<CartItem[]>([]);
itemCount = computed(() => this.cartItems().length);
totalPrice = computed(() =>
this.cartItems().reduce((sum, item) => sum + item.price, 0)
);
addItem(item: CartItem) {
this.cartItems.update((items) => [...items, item]);
}
removeItem(id: string) {
this.cartItems.update((items) => items.filter((item) => item.id !== id));
}
}
Dashboard en Tiempo Real
@Component({
selector: "app-dashboard",
template: `
<div class="dashboard">
@if (isLoading()) {
<div class="loading">Loading dashboard...</div>
} @else {
<div class="metrics">
<div class="metric">
<h3>Active Users</h3>
<span>{{ activeUsers() }}</span>
</div>
<div class="metric">
<h3>Revenue</h3>
<span>{{ revenue() | currency }}</span>
</div>
</div>
}
</div>
`,
})
export class DashboardComponent {
private websocket = inject(WebSocketService);
isLoading = signal(true);
activeUsers = signal(0);
revenue = signal(0);
ngOnInit() {
// Conexión WebSocket reactiva
this.websocket.connect().subscribe((data) => {
this.activeUsers.set(data.activeUsers);
this.revenue.set(data.revenue);
this.isLoading.set(false);
});
}
}
Roadmap y Futuro
Próximas Características
- Angular 21: Signals completamente integrados en Forms y Router
- Universal Zoneless: Zoneless estable en todas las plataformas
- Mejor Tree Shaking: Optimizaciones adicionales de bundle
- Nuevas Primitivas: Más primitives reactivas
Migración Continua
// Estrategia de migración gradual
export class MigrationStrategy {
// Fase 1: Migrar a control flow
migrateControlFlow() {
// ng generate @angular/core:control-flow
}
// Fase 2: Adoptar Signals gradualmente
adoptSignals() {
// Migrar propiedades críticas primero
}
// Fase 3: Habilitar Zoneless en producción
enableZoneless() {
// Cuando esté estable en Angular 21+
}
}
Conclusión
Angular 20 representa un paso significativo hacia un futuro zoneless y una experiencia de desarrollo más robusta. Las principales mejoras incluyen:
🎯 Beneficios Clave
- Performance Superior: Arquitectura Zoneless reduce overhead
- Mejor Developer Experience: Signals simplifican la reactividad
- Código Más Limpio: Control flow nativo más legible
- Testing Modernizado: Vitest ofrece mejor velocidad y UX
- Future-Proof: Base sólida para futuras innovaciones
📈 Impacto en Desarrollo
- Reducción de Bugs: Reactividad más predecible con Signals
- Mejor Performance: Aplicaciones más rápidas y eficientes
- Mantenimiento Simplificado: Código más declarativo y legible
- Ecosistema Moderno: Herramientas actualizadas y optimizadas
🚀 Recomendaciones
- Migrar gradualmente: Comenzar con control flow y luego Signals
- Adoptar Zoneless: En proyectos nuevos para mejor performance
- Actualizar testing: Migrar de Karma a Vitest gradualmente
- Seguir el roadmap: Prepararse para Angular 21 y características futuras
Angular 20 no solo es una actualización, es una transformación que posiciona al framework para la próxima década del desarrollo web. La inversión en estas nuevas características se traduce directamente en aplicaciones más rápidas, código más mantenible y una mejor experiencia tanto para desarrolladores como usuarios finales.
Recursos Adicionales
- Documentación Oficial: angular.dev
- Guía de Migración: update.angular.io
- Signals Guide: angular.dev/guide/signals
- Zoneless Preview: angular.dev/guide/experimental/zoneless
- Style Guide: angular.dev/style-guide