Angular 18: Abschied von zone.js auf Raten

Das aktuelle Release bereitet sich auf die Change Detection ohne zone.js vor – eine bedeutende Weichenstellung für die Zukunft des Frameworks.

In Pocket speichern vorlesen Druckansicht 2 Kommentare lesen
Viele Eisenbahnschienen vor Sonnenuntergang

(Bild: Patrick Poendl/Shutterstock.com)

Lesezeit: 12 Min.
Von
  • Rainer Hahnekamp
Inhaltsverzeichnis

Angular 18 bringt viele kleinere Neuerungen, die die Developer Experience verbessern. Die Scheinwerfer sind jedoch auf das Zoneless-Feature gerichtet. Zoneless bedeutet, dass sich Angular von der bisherigen Art und Weise verabschiedet, das DOM (Document Object Model) zu aktualisieren, und einen neuen Mechanismus einbaut. Derartige interne Umwälzungen haben eine enorme Auswirkung, weshalb zoneless zunächst als experimentell eingestuft ist.

Rainer Hahnekamp

Rainer Hahnekamp ist Trainer und Berater im Expertennetzwerk von AngularArchitects.io und dort für Schulungen rund um Angular verantwortlich. Darüber hinaus gibt er mit ng-news auf YouTube einen wöchentlichen Kurzüberblick über relevante Ereignisse im Angular-Umfeld.

Jedes Frontend-Framework braucht einen Mechanismus, um den Status der Anwendung im Browser zu aktualisieren – umgangssprachlich auch Rendern genannt. React hat das Virtual DOM, Angular verwendet die sogenannte Change Detection.

Damit die Change Detection startet, benötigt sie einen Auslöser. Vor Angular 18 waren das immer DOM-Events, die einen Event Listener sowie die Beendigung von asynchronen Tasks umfassten. Die Bibliothek zone.js überwacht diese beiden Ereignistypen und löst dabei immer die Change Detection aus. Diese läuft asynchron und aktualisiert das DOM entsprechend. Dafür muss zone.js große Teile des Browsers patchen. Obwohl zone.js eine eigenständige Bibliothek ist, ist sie fundamental für Angular und kommt deswegen seit Beginn an zum Einsatz. Mit Ereignistypen sind an dieser Stelle ein DOM-Event und die Beendigung eines asynchronen Tasks gemeint. Für die Bibliothek zone.js muss es neben einem DOM-Event auch einen zugehörigen Event Listener geben, der auf das DOM-Event Code ausführt.

Auslöser für Change Detection

(Bild: Rainer Hahnekamp)

Wie der Name schon sagt, bedeutet zoneless, dass zone.js nicht mehr verwendet wird und somit auch nicht mehr die Change Detection auslöst. Die Deaktivierung von zone.js ist mit Angular 18 noch experimentell. Der Befehl provideExperimentalZonelessChangeDetection aktiviert zoneless.

Jetzt stellt sich die Frage, was stattdessen die Change Detection auslöst. Hierfür ist eine genauere Erklärung der OnPush-Methode notwendig.

OnPush ist eine Einstellung für die Change Detection und in Angular nichts Neues. Sie gilt als Standardmethode, um die Leistung einer Angular-Anwendung zu steigern, und viele Projekte setzen sie standardmäßig ein.

Die Change Detection synchronisiert das DOM, indem sie die Werte der Properties in Komponenten mit denen im DOM vergleicht. Sollte in der Komponente ein anderer Wert auftauchen, wird automatisch die entsprechende Stelle im DOM aktualisiert.

Standardmäßige Change Detection in Angular

(Bild: Rainer Hahnekamp)

Die Change Detection überprüft jede einzelne Komponente. Dabei kann sie viele Komponenten antreffen, bei denen keine Änderung notwendig ist. Im schlimmsten Fall bleibt alles beim Alten und die Change Detection läuft umsonst – und verschwendet Ressourcen. Zum Beispiel kann ein Event Listener auf ein Click-Event nur ein console.log ausführen – keine Änderung, aber die Change Detection läuft trotzdem.

OnPush ist eine Einstellung, die eine Komponente samt ihrer Kinder von der Change Detection ausschließt. Die Change Detection überprüft diese Komponente inklusive der Kinder nur dann, wenn die OnPush-Komponente "dirty" ist. Formal heißt das, die Komponente besitzt eine Markierung für checking.

Change Detection mit OnPush

(Bild: Rainer Hahnekamp)

Die Funktion markForCheck() setzt diese Markierung.

In Angular gibt es folgende Aktionen, die intern markForCheck ausführen:

  • Die async-Pipe wird im Template für Observables verwendet.
  • Eine Elternkomponente übergibt via Property Binding (input() oder @Input) eine neue Objektreferenz an ihr Kind.
  • Es findet ein manueller Aufruf von markForCheck statt.
  • Ein Signal, das in einem Template verwendet wird, bekommt einen neuen Wert.
  • Ein Event Listener in der Komponente wird aufgerufen.

Vor Angular 18 löste markForCheck keine Change Detection aus. In den meisten Fällen merkte man allerdings keinen Unterschied, da bei allen oben genannten Kriterien zuvor entweder ein DOM-Event auftrat oder ein asynchroner Task lief, wodurch zone.js bereits die Change Detection auslöste.

Das Problem mit OnPush war deswegen sehr selten eine fehlende Change Detection, sondern bestand darin, dass die Komponente nicht als "dirty" markiert wurde. Klassische Beispiele waren Property Bindings, bei denen die Variable mutable Änderungen hatte. Es wurde also keine neue Objektreferenz erstellt.

Da markForCheck bei OnPush ohnehin schon immer den Start der Change Detection bedeutete, gilt das auch für Angular 18: markForCheck markiert nicht nur die Komponente als "dirty", sondern löst auch die Change Detection – wie zone.js – asynchron aus.

Man könnte vermuten, dass das im Grunde dieselben Kriterien sind, auf die zone.js bereits reagiert. Auf die DOM-Events trifft das zu, nicht jedoch auf die Beendigung von asynchronen Tasks. Vor allem hier traten mit OnPush in der Vergangenheit Probleme auf, die ein manuelles Ausführen von markForCheck erfordern konnten.

Ab Angular 18 macht es keinen Unterschied, ob zone.js läuft oder nicht. markForCheck löst immer die Change Detection aus. Dieses Verhalten ist ein Breaking Change, der die Funktion provideZoneChangeDetection({ ignoreChangesOutsideZone: true }) deaktivieren kann.

provideZoneChangeDetection() ist der Standard. Das heißt, zone.js und markForCheck lösen die Change Detection aus. Dadurch, dass es nun zwei Auslöser gibt, spricht das Angular-Team hier vom hybriden Modus.

Die Change Detection wird dadurch allerdings nicht doppelt ausgeführt, denn sie läuft asynchron. Sie lässt sich zwar mehrfach synchron anfordern, aber auch dann wird sie nur einmal ausgeführt.

Der hybride Modus bietet mit zone.js als Fallback die Möglichkeit an, Anwendungscode auf zoneless sicher umzustellen. Eine Anwendung, deren Komponenten alle mit OnPush ausgestattet sind, dürfte die beste Voraussetzung für zoneless haben. Der Grund liegt darin, dass mit OnPush die Komponenten als "dirty" markiert sein müssen, um ihre Änderungen entsprechend im DOM zu sehen.

Eine Deaktivierung von zone.js wird jedoch vorerst nicht möglich sein, weil auch alle Fremdbibliotheken, die im Einsatz sind, intern erst auf ein Leben ohne zone.js vorbereitet werden müssen.

Bei einer Zoneless-Anwendung läuft die Change Detection nur mehr bei markForCheck(), also wenn sich tatsächlich in einer Komponente eine Änderung ergeben hat und daher ein Update des DOMs erforderlich ist. Daraus ergeben sich drei Vorteile.

Erstens ist ein Change-Detection-Lauf ohne DOM-Update nicht mehr notwendig. Der zoneless-Ansatz ermöglicht somit wesentlich performantere Anwendungen. Zweitens schrumpft durch den Wegfall der Bibliothek zone.js das initiale JavaScript-Bundle. Bei einem optimierten Build beträgt die Differenz etwa 35 kByte. Der dritte Vorteil findet sich in der Kompilierung von Code, der mit async/await läuft. Zone.js kann nur asynchrone Tasks überwachen, die mit einem Promise funktionieren. Daher konnte man nie nativ async/await ausliefern. Auch das ist jetzt Geschichte.