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.
- Rainer Hahnekamp
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.
Zoneless-Feature in Angular 18
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.
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.
Hintergrund: Die OnPush-Methode im Detail
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.
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
.
Die Funktion markForCheck()
setzt diese Markierung.
In Angular gibt es Aktionen, die intern markForCheck
ausfĂĽhren. Zum Beispiel:
- 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.
Hybrider Modus als Übergangslösung
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.
Wieso zoneless?
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.