S

super-dev — full-stack

@super-dev.app · Full-stack · ★ open to work
12.8k subscribers·47 videos·Active since 2017

Full-stack .NET / Angular dev with a DevOps streak on Azure Cloud. Also fluent in Flutter + Firebase. I build products, ship them, and keep them healthy.

Back to articles
ANGULAR
ANGULAR

Take an Angular app zoneless + signals

Article 1 of 4 — Angular 21 in practice
Zoneless, signals, resource(), @defer, SignalStore — the modern front end, for real.

For years, Angular relied on zone.js to know when to re-run change detection: a monkey patch over every async browser API. It works, but it's an expensive black box. Since Angular 21, you can drop it entirely with provideZonelessChangeDetection() and let signals drive reactivity instead.

Why ditch zone.js

zone.js intercepts setTimeout , promises, DOM events, and triggers a global detection cycle every time. On a large app, you re-check thousands of bindings when only three changed. Zoneless flips the logic: nothing re-renders until a signal read in the template has notified its change.

  • lighter bundle: you remove a ~100 kB dependency
  • readable stack traces: no more zone.run frames everywhere
  • targeted detection: only the components that depend on the changed signal get marked

Enabling zoneless

It all happens in the application config. You remove provideZoneChangeDetection and wire the zoneless provider:

TypeScript
1import { provideZonelessChangeDetection } from '@angular/core';
2import { bootstrapApplication } from '@angular/platform-browser';
3
4bootstrapApplication(AppComponent, {
5 providers: [
6 provideZonelessChangeDetection(),
7 provideRouter(routes),
8 ],
9});

Thinking in fine-grained reactivity

Once zoneless, all state must be a signal or the template stops updating. You replace mutable fields with signal() , derived values with computed() , and side effects with effect() :

TypeScript
1@Component({
2 selector: 'app-cart',
3 changeDetection: ChangeDetectionStrategy.OnPush,
4 template: `Total: {{ total() }} €`,
5})
6export class CartComponent {
7 protected readonly items = signal<CartItem[]>([]);
8 protected readonly total = computed(() =>
9 this.items().reduce((sum, item) => sum + item.price * item.quantity, 0),
10 );
11}

Gotchas to know

Legacy code that does setTimeout(() => this.value = x) without going through a signal will no longer refresh the view. Same for RxJS subscriptions: you either use toSignal() or call signal.set() inside the subscribe . On the test side, switch Vitest to zoneless and replace fakeAsync / tick with await fixture.whenStable() . The official docs cover each case in the zoneless guide .

Zoneless doesn't magically make an app faster. It makes reactivity explicit : you know exactly why something re-renders, and that's what changes everything when debugging.
super-dev — portfolio.app
// More in ANGULAR
A DOOM-style engine in the browser, with three backends
11 min • 1.4k reads
Loading data with resource() and httpResource()
7 min • 3.6k reads
@defer and the new control flow: less JS at startup
6 min • 2.9k reads