paint-brush
So beschleunigen Sie Ihre Angular-App mit Web Workernvon@mesciusinc
Neue Geschichte

So beschleunigen Sie Ihre Angular-App mit Web Workern

von MESCIUS inc.16m2024/06/26
Read on Terminal Reader

Zu lang; Lesen

Erfahren Sie, wie Sie Ihre lang laufenden Prozesse zu einem Angular Web Worker hinzufügen und alle Vorteile einer App nutzen, die nicht einfriert.
featured image - So beschleunigen Sie Ihre Angular-App mit Web Workern
MESCIUS inc. HackerNoon profile picture

Warum brauchen Sie einen Web Worker ? Ein Web Worker ist eine Codekomponente für eine Webanwendung. Er ermöglicht dem Entwickler, einen neuen Ausführungsthread für eine JavaScript-Aufgabe zu erstellen, sodass die Ausführung der Hauptanwendung nicht unterbrochen wird.


Auf den ersten Blick könnte man meinen, dass Browser Threading von Haus aus unterstützen und der Entwickler nichts Besonderes tun muss. Leider ist das nicht der Fall. Web Worker lösen ein echtes Parallelitätsproblem.


Web Worker sind Teil der erwarteten Funktionsstandards von Webbrowsern und die Spezifikationen dafür wurden beim W3C verfasst. Das Angular- Framework hat Web Worker für uns fertig gestellt und wir können sie mithilfe der Angular-Befehlszeilenschnittstelle (CLI) ganz einfach zu unserer App hinzufügen.


In diesem Artikel untersuchen wir zunächst einige Missverständnisse über Thread-Parallelität mit JavaScript im Browser. Anschließend erstellen wir ein funktionales Beispiel, das zeigt, wie einfach es ist, Web Worker mit Angular zu implementieren, was paralleles Threading auf einer Website ermöglicht.

Ist JavaScript nicht von Natur aus parallel?

Einige Entwickler glauben, dass JavaScript im Browser von Natur aus parallel ausgeführt wird, da der Browser, wenn er eine Verbindung zu einer Website herstellt und das HTML für eine Seite abruft, mehrere Verbindungen (etwa sechs) öffnen und gleichzeitig Ressourcen (Bilder, verknüpfte CSS-Dateien, verknüpfte JavaScript-Dateien usw.) abrufen kann. Es sieht so aus, als ob der Browser mehrere Threads und zahlreiche Aufgaben gleichzeitig ausführt (über Kontextwechsel).


Für den unerfahrenen Webentwickler scheint dies darauf hinzudeuten, dass der Browser parallele Arbeiten ausführen kann. Bei JavaScript führt der Browser jedoch tatsächlich immer nur einen Prozess gleichzeitig aus.


Die meisten modernen Websites, Single Page Apps (SPA) und die moderneren Progressive Web Apps (PWA) basieren auf JavaScript und enthalten in der Regel zahlreiche JavaScript-Module. Wenn die Web-App jedoch JavaScript ausführt, ist der Browser auf einen einzigen Aktivitätsthread beschränkt. Unter normalen Umständen wird kein JavaScript gleichzeitig ausgeführt.


Das bedeutet, wenn wir in einem unserer JavaScript-Module eine lang andauernde oder prozessintensive Aufgabe definiert haben, kann es sein, dass der Benutzer das Gefühl hat, die App stottert oder hängt. Gleichzeitig wartet der Browser, bis der Prozess abgeschlossen ist, bevor die Benutzeroberfläche (UI) aktualisiert werden kann. Durch dieses Verhalten verlieren Benutzer das Vertrauen in unsere Web-Apps oder SPAs, und das möchte niemand von uns.

Ein Web Worker für JavaScript-Parallelität

Wir erstellen eine Beispielseite mit zwei Bereichen, damit unsere Web-App gleichzeitige JavaScript-Threads ausführen kann. In einem Bereich wird die Benutzeroberfläche unserer Anwendung durch ein Raster mit Kreisen dargestellt, das das Bild ständig aktualisiert und auf Mausklicks reagiert. Der zweite Bereich beherbergt einen lang laufenden Prozess, der normalerweise den UI-Thread blockiert und die Benutzeroberfläche daran hindert, ihre Aufgabe zu erfüllen.


Gleichzeitige JavaScript-Threads


Damit unsere Benutzeroberfläche reaktionsfähig ist, führen wir unseren langen Prozess in einem Web Worker aus, der ihn in einem separaten Thread ausführt und die Ausführung der Benutzeroberflächenlogik auf diese Weise nicht blockiert. Wir verwenden Angular als Framework zum Erstellen der App, da die Erstellung der Web Worker damit zu einer einfachen Aufgabe mit nur einem Befehl wird.

Einrichten der Angular-App

Um die Angular CLI zu verwenden, müssen Node.js und NPM (Node Package Manager) installiert sein. Sobald wir sichergestellt haben, dass Node und NPM installiert sind, öffnen Sie ein Konsolenfenster und installieren Sie die Angular CLI über NPM (dies ist eine einmalige Sache):


 npm install -g @angular/cli


Wechseln Sie zum Zielverzeichnis, in dem wir unsere neue App-Vorlage erstellen möchten. Jetzt können wir unsere App erstellen. Dazu verwenden wir den Befehl „ng new“. Wir nennen unser Projekt NgWebWorker:


 ng new NgWebWorker --no-standalone


Der Projektassistent fragt, ob wir Routing in unser Projekt aufnehmen möchten. Für dieses Beispiel benötigen wir kein Routing, also geben Sie n ein.


Anschließend wird gefragt, welche Art von Stylesheet-Format wir verwenden möchten. Angular unterstützt die Verwendung von Stylesheet-Prozessoren wie Sass und Less, aber in diesem Fall verwenden wir einfaches CSS. Drücken Sie also einfach die Eingabetaste, um die Standardeinstellung zu erhalten.


Stylesheet-Format


Wir werden dann einige CREATE-Meldungen sehen, während NPM die erforderlichen Pakete abruft und die CLI das Vorlagenprojekt erstellt. Wenn es abgeschlossen ist, erhalten wir in der Befehlszeile erneut einen blinkenden Cursor.


An diesem Punkt hat die Angular CLI ein Projekt erstellt und es im Ordner NgWebWorker abgelegt. Ändern Sie das Verzeichnis in NgWebWorker. Die Angular CLI hat alles erstellt, was wir für die Arbeit an unserem Angular-Projekt benötigten, einschließlich der Installation des Node-HTTP-Servers. Das bedeutet, dass wir zum Starten der Vorlagen-App nur Folgendes tun müssen:


 ng serve 


Angular-Befehlszeilenschnittstelle


Die Angular CLI kompiliert Ihr Projekt und startet den Node-HTTP-Server. Jetzt können Sie die App in Ihrem Browser laden, indem Sie auf die URL <a href="http://localhost:4200"target="_blank"> zeigen.

Unsere erste Angular-App von CLI

Wenn wir die Seite laden, sehen wir oben die Basisvorlage mit dem Projektnamen.


Der Vorteil beim Ausführen von „ng serve“ besteht darin, dass alle am Code vorgenommenen Änderungen automatisch zu einer Aktualisierung der Site im Browser führen. Dadurch ist die Wirkung der Änderungen viel einfacher zu erkennen.


Der Großteil des Codes, auf den wir uns konzentrieren, befindet sich im Verzeichnis /src/app.


/src/app-Verzeichnis


app.component.html enthält HTML für die Komponente, die derzeit zum Anzeigen der Hauptseite verwendet wird. Der Komponentencode wird in der Datei app.component.ts (TypeScript) dargestellt.


Wir löschen den Inhalt von app.component.html und fügen unser eigenes Layout hinzu. Wir erstellen eine geteilte Seite, die auf der linken Seite unsere lang laufenden Prozesswerte anzeigt und auf der rechten Seite einige zufällige Kreise zeichnet. Dadurch kann unser Browser zwei zusätzliche Web Worker-Threads ausführen, die unabhängig voneinander arbeiten, sodass Sie Angular Web Workers in Aktion sehen können.


Der gesamte Code für den Rest des Artikels kann aus dem GitHub-Repository abgerufen werden.


So sieht die endgültige Seite aus, während die zufälligen Kreise gezeichnet werden (bevor der zeitintensive Vorgang beginnt).


Beschleunigen Sie Ihre Angular-App mit Web Workern


Ersetzen Sie den Code in app.component.html durch Folgendes:


 <div id="first"> <div class="innerContainer"> <button (click)="longLoop()">Start Long Process</button> </div> <div class="innerContainer"> <textarea rows="20" [value]="(longProcessOutput)"></textarea> </div> </div>


Der Code-Download enthält außerdem einige IDs und CSS-Klassen sowie die dazugehörigen Styles in styles.css , die zur sehr einfachen Formatierung der UI dienen, sodass wir zwei Bereiche (links und rechts) und weitere grundlegende Styling-Elemente haben.


 .container { width: 100%; margin: auto; padding: 1% 2% 0 1%; } .innerContainer{ padding: 1%; } #first { width: 50%; height: 405px; float:left; background-color: lightblue; color: white; } #second { width: 50%; float: right; background-color: green; color: white; }


Wichtig ist hier, dass wir der Schaltfläche eine Angular-Ereignisbindung (Klick) hinzugefügt haben. Wenn der Benutzer auf die Schaltfläche klickt, wird der lange Prozess durch Aufrufen der longLoop-Methode gestartet, die sich in der TypeScript-Komponentendatei app.component.ts befindet.


 title = 'NgWebWorker'; longProcessOutput: string = 'Long\nprocess\noutput\nwill\nappear\nhere\n'; fibCalcStartVal: number; longLoop() { this.longProcessOutput = ''; for (var x = 1; x <= 1000000000; x++) { var y = x / 3.2; if (x % 20000000 == 0) { this.longProcessOutput += x + '\n'; console.log(x); } } }


Dies führt 10 Milliarden Iterationen aus und schreibt in eine Mitgliedsvariable unserer Komponente, longProcessOutput.


Da wir diese Membervariable in app.component.html (an das Textbereichselement) gebunden haben, wird die Benutzeroberfläche die Aktualisierung jedes Mal widerspiegeln, wenn die Variable aktualisiert wird. Der Wert, den wir im HTML festlegen, ist der Ort, an dem wir die Membervariable binden.


 <textarea rows="20" [value]="longProcessOutput"></textarea>


Führen Sie es aus. Wir werden sehen, dass nicht viel passiert, wenn wir auf die Schaltfläche klicken, und dann wird der Textbereich plötzlich mit einer Reihe von Werten aktualisiert. Wenn wir die Konsole öffnen, sehen wir die dort geschriebenen Werte, während der Code ausgeführt wird.

Hinzufügen einer zufälligen Kreiskomponente mithilfe der Angular CLI

Als nächstes fügen wir eine „Kreis“-Komponente hinzu, um zufällige Kreise zu zeichnen. Wir können das über die Angular CLI mit dem folgenden Befehl tun:


 ng generate component circle


Der Befehl erstellte einen neuen Ordner mit dem Namen „circle“ und erstellte vier neue Dateien:

  • kreis.komponente.html

  • circle.component.spec.ts (Einheitentests)

  • circle.component.ts (TypeScript-Code)

  • circle.component.css (Stile, die nur auf das zugehörige HTML für diese Komponente angewendet werden)


Bauteilkreis erzeugen


Das HTML ist unkompliziert. Wir brauchen nur das HTML, das unsere Komponente darstellt. In unserem Fall ist dies die Komponente auf der rechten Seite der Seite, die das hellgrüne Raster anzeigt und die Kreise zeichnet. Diese Zeichnung erfolgt über das HTML-Canvas-Element.


 <div id="second"> <canvas #mainCanvas (mousedown)="toggleTimer()"></canvas> </div>


Wir starten und stoppen das Zeichnen der Kreise, indem wir eine Angular-Ereignisbindung hinzufügen, um das Mousedown-Ereignis zu erfassen. Wenn der Benutzer irgendwo im Canvas-Bereich klickt, werden die Kreise gezeichnet, sofern der Vorgang nicht bereits gestartet wurde. Wenn der Vorgang bereits gestartet ist, löscht die Methode toggleTimer (zu finden in circle.component.ts ) das Raster und stoppt das Zeichnen der Kreise.


toggleTimer verwendet einfach setInterval, um alle 100 Millisekunden (10 Kreise/Sekunde) einen Kreis an einer zufälligen Stelle mit einer zufällig ausgewählten Farbe zu zeichnen.


 toggleTimer(){ if (CircleComponent.IntervalHandle === null){ CircleComponent.IntervalHandle = setInterval(this.drawRandomCircles,50); } else{ clearInterval(CircleComponent.IntervalHandle); CircleComponent.IntervalHandle = null; this.drawGrid(); } }


In circle.component.ts gibt es weiteren Code, der das Canvas-Element einrichtet, Mitgliedsvariablen initialisiert und die Zeichnung durchführt. Nach dem Hinzufügen sollte Ihr Code folgendermaßen aussehen:


 import { ViewChild, Component, ElementRef, AfterViewInit } from '@angular/core'; @Component({ selector: 'app-circle', templateUrl: './circle.component.html', styleUrl: './circle.component.css', }) export class CircleComponent implements AfterViewInit { title = 'NgWebWorker'; static IntervalHandle = null; static ctx: CanvasRenderingContext2D; GRID_LINES: number = 20; lineInterval: number = 0; gridColor: string = 'lightgreen'; static CANVAS_SIZE: number = 400; @ViewChild('mainCanvas', { static: false }) mainCanvas: ElementRef; constructor() { console.log('ctor complete'); } ngAfterViewInit(): void { CircleComponent.ctx = (<HTMLCanvasElement>( this.mainCanvas.nativeElement )).getContext('2d'); this.initApp(); this.initBoard(); this.drawGrid(); this.toggleTimer(); } initApp() { CircleComponent.ctx.canvas.height = CircleComponent.CANVAS_SIZE; CircleComponent.ctx.canvas.width = CircleComponent.ctx.canvas.height; } initBoard() { console.log('initBoard...'); this.lineInterval = Math.floor( CircleComponent.ctx.canvas.width / this.GRID_LINES ); console.log(this.lineInterval); } drawGrid() { console.log('drawGrid...'); CircleComponent.ctx.globalAlpha = 1; // fill the canvas background with white CircleComponent.ctx.fillStyle = 'white'; CircleComponent.ctx.fillRect( 0, 0, CircleComponent.ctx.canvas.height, CircleComponent.ctx.canvas.width ); for (var lineCount = 0; lineCount < this.GRID_LINES; lineCount++) { CircleComponent.ctx.fillStyle = this.gridColor; CircleComponent.ctx.fillRect( 0, this.lineInterval * (lineCount + 1), CircleComponent.ctx.canvas.width, 2 ); CircleComponent.ctx.fillRect( this.lineInterval * (lineCount + 1), 0, 2, CircleComponent.ctx.canvas.width ); } } toggleTimer() { if (CircleComponent.IntervalHandle === null) { CircleComponent.IntervalHandle = setInterval(this.drawRandomCircles, 100); } else { clearInterval(CircleComponent.IntervalHandle); CircleComponent.IntervalHandle = null; this.drawGrid(); } } static generateRandomPoints() { var X = Math.floor(Math.random() * CircleComponent.CANVAS_SIZE); // gen number 0 to 649 var Y = Math.floor(Math.random() * CircleComponent.CANVAS_SIZE); // gen number 0 to 649 return { x: X, y: Y }; } drawRandomCircles() { var p = CircleComponent.generateRandomPoints(); CircleComponent.drawPoint(p); } static drawPoint(currentPoint) { var RADIUS: number = 10; var r: number = Math.floor(Math.random() * 256); var g: number = Math.floor(Math.random() * 256); var b: number = Math.floor(Math.random() * 256); var rgbComposite: string = 'rgb(' + r + ',' + g + ',' + b + ')'; CircleComponent.ctx.strokeStyle = rgbComposite; CircleComponent.ctx.fillStyle = rgbComposite; CircleComponent.ctx.beginPath(); CircleComponent.ctx.arc( currentPoint.x, currentPoint.y, RADIUS, 0, 2 * Math.PI ); // allPoints.push(currentPoint); CircleComponent.ctx.stroke(); CircleComponent.ctx.fill(); } }


Vergessen Sie nicht, die Kreiskomponente zur Datei index.html hinzuzufügen:


 <body> <app-root></app-root> <app-circle></app-circle> </body>


Wenn die Seite geladen ist, werden die Kreise gezeichnet. Wenn wir auf die Schaltfläche [Langen Prozess starten] klicken, wird das Zeichnen angehalten. Das liegt daran, dass die gesamte Arbeit im selben Thread ausgeführt wird.


Beheben wir dieses Problem, indem wir einen Web Worker hinzufügen.

Hinzufügen eines Angular Web Worker

Um einen neuen Web Worker mithilfe der CLI hinzuzufügen, gehen wir einfach zu unserem Projektordner und führen den folgenden Befehl aus:


 ng generate web-worker app


Dieser letzte Parameter (App) ist der Name der Komponente, die unseren lang laufenden Prozess enthält, den wir in unseren Web Worker einfügen möchten.


Web-Worker-App generieren


Angular fügt der Datei app.component.ts Code hinzu, der wie folgt aussieht:


 if (typeof Worker !== 'undefined') { // Create a new const worker = new Worker(new URL('./app.worker', import.meta.url)); worker.onmessage = ({ data }) => { console.log(`page got message: ${data}`); }; worker.postMessage('hello'); } else { // Web Workers are not supported in this environment. // You should add a fallback so that your program still executes correctly. }


Was macht der neue Code? Wir können sehen, dass dieser Code auf die neue app.worker-Komponente verweist, die der Befehl ebenfalls hinzugefügt hat. An dieser Stelle sieht der Code so aus:


  1. Stellt sicher, dass der Browser Web Worker unterstützt.
  2. Erstellt einen neuen Worker.
  3. Postet eine Nachricht an den Worker (zu finden in app.worker.ts ).
  4. Wenn der Worker die „Hallo“-Nachricht erhält, wird EventListener ausgelöst (siehe folgendes Code-Snippet).
  5. Wenn der EventListener ausgelöst wird (in app.worker.ts ), erstellt er ein Antwortobjekt und sendet es an den Anrufer zurück.


Hier ist der gesamte Inhalt von app.worker.ts :


 /// <reference lib="webworker" /> addEventListener('message', ({ data }) => { const response = `worker response to ${data}`; postMessage(response); });


Als Ergebnis dieser Schritte werden in der Konsole Meldungen angezeigt, die wie die letzte Zeile in der folgenden Konsolenausgabe aussehen:


Konsolenausgabe


Dies ist das console.log, das im EventHandler auftritt, der auf dem ursprünglichen Worker-Objekt erstellt wurde:


 worker.onmessage = ({ data }) => { console.log(`page got message: ${data}`); };


Dies bedeutet, dass die App-Komponente eine Nachricht an den App-Worker gesendet hat und der App-Worker mit einer eigenen Nachricht geantwortet hat.


Wir möchten den Worker verwenden, um unseren lang laufenden Prozess auf einem anderen Thread auszuführen, damit unser Code zum Zeichnen von Kreisen nicht unterbrochen wird.


Verschieben wir zunächst den Code, der mit unseren UI-Elementen zu tun hat, in einen Konstruktor unserer app.component-Klasse.


 constructor() { if (typeof Worker !== 'undefined') { // Create a new const worker = new Worker(new URL('./app.worker', import.meta.url)); worker.onmessage = ({ data }) => { console.log(`page got message: ${data}`); this.longProcessOutput += `page got message: ${data}` + '\n'; }; worker.postMessage('hello'); } else { // Web Workers are not supported in this environment. // You should add a fallback so that your program still executes correctly. } }


Dadurch können wir nun auf die Variable longProcessOutput verweisen. Damit können wir auf diese Variable zugreifen; wir haben worker.onmessage, das die grundlegenden Daten dem Textbereich hinzufügt, anstatt sie in die Konsole zu schreiben, nur als ersten Test.


Sie können sehen, dass der hervorgehobene Text links die empfangene Nachricht ist.


Empfangene Nachricht

LongLoop im Web Worker

Wir müssen unsere lang laufende Schleife noch zum Web Worker verschieben, um sicherzustellen, dass die Schleife bei Ausführung auf ihrem eigenen Thread ausgeführt wird.


Hier ist der Großteil des Codes, den wir in unserem endgültigen app.component.ts haben werden:


 constructor() { if (typeof Worker !== 'undefined') { // Create a new const worker = new Worker(new URL('./app.worker', import.meta.url)); worker.onmessage = ({ data }) => { console.log(`page got message: ${data}`); this.longProcessOutput += `page got message: ${data}` + '\n'; }; worker.postMessage('hello'); } else { // Web Workers are not supported in this environment. // You should add a fallback so that your program still executes correctly. } } longLoop() { this.longProcessOutput = ''; for (var x = 1; x <= 1000000000; x++) { var y = x / 3.2; if (x % 20000000 == 0) { this.longProcessOutput += x + '\n'; console.log(x); } } }


Wir haben die Worker-Variable in die Klasse verschoben, die jetzt eine Member-Variable ist. Auf diese Weise können wir sie überall in unserer AppComponent-Klasse problemlos referenzieren.


Als nächstes schauen wir uns genauer an, wie wir mit dem Code im Konstruktor einen Nachrichtenereignishandler für das Worker-Objekt definiert haben:


 this.worker.onmessage = ({ data }) => { this.longProcessOutput += `${data}` + "\n"; };


Dieser Code wird ausgeführt, wenn die Web Worker-Klasse (zu finden in app.worker.ts ) postMessage(data) aufruft. Bei jedem Aufruf der postMessage-Methode wird longProcessOutput (das an den Textbereich gebundene Modell) mit den Daten plus einem Wagenrücklauf („\n“) aktualisiert, sodass jeder Wert einfach in eine eigene Zeile im Textbereichselement geschrieben wird.


Hier ist der gesamte Code, der im eigentlichen Web Worker ( app.worker.ts ) zu finden ist:


 addEventListener('message', ({ data }) => { console.log(`in worker EventListener : ${data}`); for (var x = 1; x <=1000000000;x++){ var y = x/3.2; if ((x % 20000000) == 0){ // posts the value back to our worker.onmessage handler postMessage(x); // don't need console any more --> console.log(x); } } });


Dieser Eventhandler wird ausgelöst, wenn der Benutzer auf die Schaltfläche [Langen Prozess ausführen] klickt. Der gesamte Code im Web Worker ( app.worker.ts ) wird in einem neuen Thread ausgeführt. Das ist der Wert des Web Workers; sein Code wird in einem separaten Thread ausgeführt. Deshalb wirkt er sich nicht mehr auf den Hauptthread der Web-App aus.


Der Web Worker-Code wird ausgelöst, wenn der Benutzer auf die Schaltfläche klickt, da wir jetzt den folgenden Code in unserer LongLoop-Methode haben.


 longLoop(){ this.longProcessOutput = ""; // the following line starts the long process on the Web Worker // by sending a message to the Web Worker this.worker.postMessage("start looping..."); }


Wenn die Nachricht an den Worker gesendet wird, wird der EventListener ausgelöst und führt den Code aus unserem ursprünglichen LongLoop aus, den wir dort platziert haben.

Angular-Apps mit Web Workern fertigstellen

Wenn Sie die App ausführen, werden Sie feststellen, dass das Klicken auf die Schaltfläche [Langen Prozess starten] nicht mehr dazu führt, dass das Zeichnen des Kreises angehalten wird. Sie können auch direkt mit der Canvas-Komponente zum Zeichnen von Kreisen interagieren, sodass das Canvas sofort neu gezeichnet wird, wenn Sie darauf klicken, während longLoop noch ausgeführt wird. Zuvor verhielt sich die App so, als wäre sie eingefroren, wenn Sie dies taten.


Jetzt können Sie Ihre lang laufenden Prozesse zu einem Angular Web Worker hinzufügen und alle Vorteile einer App nutzen, die nicht einfriert.


Wenn Sie ein mit JavaScript Web Workers gelöstes Beispiel sehen möchten, können Sie es auf Plunker sehen.


Suchen Sie nach Framework-unabhängigen UI-Komponenten? MESCIUS bietet einen vollständigen Satz von JavaScript-UI-Komponenten, darunter Datenraster, Diagramme, Messinstrumente und Eingabesteuerelemente . Wir bieten außerdem leistungsstarke Tabellenkalkulationskomponenten , Berichtssteuerelemente und verbesserte Präsentationsansichten .


Wir bieten umfassende Unterstützung für Angular (sowie React und Vue) und sind bestrebt, unsere Komponenten für die Verwendung in modernen JavaScript-Frameworks zu erweitern.