Javi Moreno
Angular 20: Revolución en Reactividad y Performance - Todas las Novedades

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

  1. Migrar gradualmente: Comenzar con control flow y luego Signals
  2. Adoptar Zoneless: En proyectos nuevos para mejor performance
  3. Actualizar testing: Migrar de Karma a Vitest gradualmente
  4. 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