Saltar al contenido
Angular•5 min de lectura

Zoneless Angular: La guia definitiva

Hace poco estuve probando con Angular v22 y me di cuenta de algo: si tu proyecto aun depende de Zone.js, estas dejando rendimiento sobre la mesa. En serio. Zoneless ya es el comportamiento por defecto desde Angular v21, y si no has migrado, te estas perdiendo mejoras significativas en performance, debugging y compatibilidad.

Vamos a desglosar que significa esto y como hacer la transicion sin que te de un infarto.


Que era Zone.js y por que se fue

Zone.js era el motor invisible que hacia funcionar la deteccion de cambios en Angular. Funcionaba parcheando APIs del navegador: cada vez que ocurria un evento del DOM, un setTimeout, o una Promise, Zone.js le avisaba a Angular: "Oye, algo paso, revisa si algo cambio".

El problema? Es como tener un vigilante que grita "FUEGO!" cada vez que alguien enciende una vela. Zone.js no sabia si realmente habia un cambio de estado, solo sabia que algo asincrono habia pasado. Resultado: deteccion de cambios innecesaria, una y otra vez.

"The main advantages to removing ZoneJS as a dependency are: Improved performance, Improved Core Web Vitals, Improved debugging experience, Better ecosystem compatibility" - Source: https://angular.dev/guide/zoneless


Los 4 beneficios concretos

1. Performance real

Sin Zone.js, Angular solo ejecuta deteccion de cambios cuando realmente hay un cambio de estado notificado. Esto se traduce en menos ciclos de renderizado y una app mas fluida.

2. Mejor Core Web Vitals

Zone.js aporta overhead tanto en tamano de payload como en tiempo de inicio. Eliminarlo mejora metricas como LCP y TTI directamente.

3. Debugging limpio

Los stack traces con Zone.js son un dolor de cabeza. Sin el, los errores apuntan directamente a tu codigo, no a las capas de parches internos.

4. Compatibilidad con el ecosistema

Zone.js parchea APIs del navegador, pero no puede parchear todo. async/await tiene que ser transformado para funcionar con el. Librerias de terceros a veces rompen por los parches. Sin Zone.js, estos problemas desaparecen.


Como funciona la deteccion de cambios sin Zone

Angular ahora depende de notificaciones explicitas para saber cuando ejecutar deteccion de cambios:

  • Signals: cuando actualizas un signal que se lee en un template, Angular sabe que tiene que actualizar.
  • markForCheck(): sigue funcionando para codigo que no usa signals.
  • AsyncPipe: sigue siendo compatible.
  • Event listeners del template: los callbacks de eventos en el template notifican automaticamente.

"Angular relies on notifications from core APIs in order to determine when to run change detection and on which views" - Source: https://angular.dev/guide/zoneless


Migracion paso a paso

Paso 1: Verifica tu version

Zoneless es el default en Angular v21+. Si estas en v22, solo necesitas verificar que nadie haya anulado la configuracion:

// main.ts - NO deberia existir esto en v22
bootstrapApplication(App, {
  providers: [
    provideZoneChangeDetection(), // SI ESTO EXISTE, ELIMINALO
  ],
});

Paso 2: Eliminar Zone.js del build

// angular.json - busca "polyfills"
{
  "projects": {
    "tu-app": {
      "architect": {
        "build": {
          "options": {
            "polyfills": [] // Elimina "zone.js" de aqui
          }
        },
        "test": {
          "options": {
            "polyfills": [] // Y de aqui tambien
          }
        }
      }
    }
  }
}

Desues, desinstala el paquete:

# terminal
npm uninstall zone.js

Paso 3: Migrar NgZone hooks

Si tu codigo usa NgZone.onMicrotaskEmpty, NgZone.onStable o NgZone.isStable, necesitas reemplazarlos:

Antes:

// app.component.ts - VIEJO
import { NgZone } from '@angular/core';

export class AppComponent {
  constructor(private ngZone: NgZone) {
    this.ngZone.onStable.subscribe(() => {
      console.log('Angular termino de actualizar');
    });
  }
}

Despues:

// app.component.ts - NUEVO
import { Component, afterEveryRender } from '@angular/core';

export class AppComponent {
  constructor() {
    afterEveryRender(() => {
      console.log('Angular termino de actualizar');
    });
  }
}

Paso 4: SSR y PendingTasks

Si usas Server-Side Rendering, Zone.js ayudaba a determinar cuando la app estaba "estable" para serializar. Ahora necesitas usar PendingTasks explicitamente:

// data.service.ts
import { Injectable, inject } from '@angular/core';
import { PendingTasks } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class DataService {
  private pendingTasks = inject(PendingTasks);

  async loadData() {
    return this.pendingTasks.run(async () => {
      const response = await fetch('/api/data');
      return response.json();
    });
  }
}

Para observables, usa pendingUntilEvent:

// data.service.ts
import { inject } from '@angular/core';
import { PendingTasks } from '@angular/core';
import { pendingUntilEvent } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';

export class DataComponent {
  private http = inject(HttpClient);

  // La app no se serializa hasta que el observable emita
  data$ = this.http.get('/api/data').pipe(pendingUntilEvent());
}

Testing sin Zone.js

El testing tambien cambia. En lugar de fixture.detectChanges(), usa await fixture.whenStable():

// app.component.spec.ts
import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';

describe('AppComponent', () => {
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [AppComponent],
    }).compileComponents();
  });

  it('should render correctly', async () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.componentInstance;

    // En vez de fixture.detectChanges()
    await fixture.whenStable();

    expect(app.title()).toBe('Mi App');
  });
});

OnPush ahora es la estrategia por defecto

Un cambio importante: Angular v22 renombro ChangeDetectionStrategy.Default a ChangeDetectionStrategy.Eager y establecio OnPush como la estrategia por defecto para nuevos componentes.

Esto significa que si creas un componente nuevo:

// nuevo.component.ts
@Component({
  selector: 'app-nuevo',
  template: `<p>{{ texto() }}</p>`,
  // changeDetection: ChangeDetectionStrategy.OnPush ya es el default
})
export class NuevoComponent {
  texto = signal('Hola mundo');
}

La trampa de Reactive Forms

Cuidado: Reactive Forms no notifican automaticamente la deteccion de cambios en modo zoneless. Si tu template depende de estado de reactive forms, necesitas conectarlo a signals o llamar markForCheck():

// form.component.ts
import { Component, ChangeDetectorRef, inject } from '@angular/core';
import { ReactiveFormsModule, FormBuilder } from '@angular/forms';

@Component({
  selector: 'app-form',
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="form">
      <input formControlName="nombre" />
      <p>{{ form.get('nombre')?.value }}</p>
    </form>
  `,
})
export class FormComponent {
  private fb = inject(FormBuilder);
  private cdr = inject(ChangeDetectorRef);

  form = this.fb.group({ nombre: [''] });

  constructor() {
    this.form.valueChanges.subscribe(() => {
      this.cdr.markForCheck(); // Necesario en zoneless
    });
  }
}

O mejor, usa Signal Forms ;).


Debug mode: tu mejor aliado

Angular ofrece una herramienta para verificar que tu app haga las notificaciones correctamente:

// main.ts
import { provideCheckNoChangesConfig } from '@angular/core';

bootstrapApplication(App, {
  providers: [
    provideCheckNoChangesConfig({
      exhaustive: true,
      interval: 1000, // Revisa cada segundo
    }),
  ],
});

Si hay bindings que se actualizaron sin notificacion, Angular lanzara ExpressionChangedAfterItHasBeenCheckedError.


Conclusiónn

Zoneless Angular no es una moda: es el futuro (y el presente) del framework. La eliminacion de Zone.js mejora performance, simplifica debugging y garantiza compatibilidad a largo plazo con las APIs del navegador.

Si aun no has migrado, empieza hoy. El camino esta documentado y las herramientas de migracion te acompanan.

Para mas contenido sobre Angular, visita nuestro blog.


Referencias adicionales:


> Más posts