paint-brush
Cómo acelerar su aplicación Angular con Web Workerspor@mesciusinc
178 lecturas

Cómo acelerar su aplicación Angular con Web Workers

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

Demasiado Largo; Para Leer

Aprenda cómo agregar sus procesos de larga duración a Angular Web Worker y obtenga todos los beneficios de una aplicación que no se congela.
featured image - Cómo acelerar su aplicación Angular con Web Workers
MESCIUS inc. HackerNoon profile picture

¿Por qué necesitas un trabajador web ? Un Web Worker es un componente de código para una aplicación web. Permite al desarrollador crear un nuevo hilo de ejecución para una tarea de JavaScript para que no interrumpa la ejecución de la aplicación principal.


A primera vista, puede parecer que los navegadores soportan inherentemente subprocesos y que el desarrollador no debería tener que hacer nada especial. Desafortunadamente, ese no es el caso. Web Workers resuelve un problema de concurrencia real.


Los Web Workers son parte de los estándares funcionales esperados de los navegadores web y sus especificaciones se redactaron en el W3C . El marco Angular nos ha incluido a los trabajadores web y podemos agregarlos fácilmente a nuestra aplicación utilizando la interfaz de línea de comandos (CLI) de Angular.


En este artículo, primero examinaremos algunos conceptos erróneos sobre la concurrencia de subprocesos con JavaScript en el navegador. Luego, crearemos un ejemplo funcional que demuestra lo fácil que es implementar Web Workers con Angular, que permite subprocesos simultáneos en un sitio web.

¿No es JavaScript inherentemente concurrente?

Algunos desarrolladores creen que JavaScript es inherentemente concurrente en el navegador porque cuando el navegador se conecta a un sitio web y recupera el HTML de una página, puede abrir múltiples conexiones (alrededor de seis) y extraer recursos (imágenes, archivos CSS vinculados, archivos JavaScript vinculados, y así sucesivamente) simultáneamente. Parece que el navegador ejecuta varios subprocesos y numerosas tareas simultáneamente (mediante cambio de contexto).


Para el desarrollador web no iniciado, esto parece indicar que el navegador puede realizar trabajos simultáneos. Sin embargo, cuando se trata de JavaScript, el navegador en realidad sólo ejecuta un proceso a la vez.


La mayoría de los sitios web modernos, las aplicaciones de página única (SPA) y las aplicaciones web progresivas (PWA) más modernas dependen de JavaScript y normalmente contienen numerosos módulos de JavaScript. Sin embargo, en cualquier momento en que la aplicación web ejecuta JavaScript, el navegador está limitado a un único hilo de actividad. Ninguno de los JavaScript se ejecutará simultáneamente en circunstancias normales.


Eso significa que si tenemos definida una tarea de larga duración o de proceso intensivo en uno de nuestros módulos de JavaScript, el usuario puede experimentar que la aplicación tartamudea o parece bloquearse. Al mismo tiempo, el navegador espera a que se complete el proceso antes de poder actualizar la interfaz de usuario (UI). Este tipo de comportamiento hace que los usuarios pierdan confianza en nuestras aplicaciones web o SPA, y ninguno de nosotros quiere eso.

Un trabajador web para la simultaneidad de JavaScript

Crearemos una página de ejemplo con dos paneles para permitir que nuestra aplicación web ejecute subprocesos de JavaScript simultáneos. En un panel, la interfaz de usuario de nuestra aplicación está representada por una cuadrícula con círculos, que actualiza constantemente la imagen y reacciona a los clics del mouse. El segundo panel albergará un proceso de larga duración, que normalmente bloquea el hilo de la interfaz de usuario e impide que la interfaz de usuario haga su trabajo.


Hilos de JavaScript simultáneos


Para que nuestra interfaz de usuario responda, ejecutaremos nuestro proceso largo en un Web Worker, que lo ejecutará en un hilo separado y no bloqueará la ejecución de la lógica de la interfaz de usuario de esta manera. Usaremos Angular como marco para crear la aplicación porque hace que la construcción de Web Workers sea una tarea sencilla de un solo comando.

Configurando la aplicación angular

Para utilizar Angular CLI, necesitamos tener instalados Node.js y NPM (Node Package Manager). Una vez que nos hayamos asegurado de que Node y NPM estén instalados, abra una ventana de consola, luego instale Angular CLI a través de NPM (esto es algo que se realiza una sola vez):


 npm install -g @angular/cli


Cambie el directorio al directorio de destino donde queremos crear nuestra nueva plantilla de aplicación. Ahora estamos listos para crear nuestra aplicación. Usamos el comando "ng new" para hacer eso. Llamaremos a nuestro proyecto NgWebWorker:


 ng new NgWebWorker --no-standalone


El asistente del proyecto nos pregunta si queremos incluir enrutamiento en nuestro proyecto. No necesitamos enrutamiento para este ejemplo, así que escriba n.


Luego nos preguntará qué tipo de formato de hoja de estilo queremos utilizar. Angular admite el uso de procesadores de hojas de estilo como Sass y Less, pero en este caso usaremos CSS simple, así que simplemente presione Enter para obtener el valor predeterminado.


Formato de hoja de estilo


Luego veremos algunos mensajes CREATE a medida que NPM extrae los paquetes necesarios y la CLI crea el proyecto de plantilla. Finalmente, cuando se completa, aparece nuevamente un cursor parpadeante en la línea de comando.


En este punto, Angular CLI creó un proyecto y lo colocó en la carpeta NgWebWorker. Cambie el directorio a NgWebWorker. La CLI de Angular creó todo lo que necesitábamos para trabajar en nuestro proyecto de Angular, incluida la instalación del servidor HTTP de Node. Eso significa que todo lo que tenemos que hacer para iniciar la aplicación de plantilla es lo siguiente:


 ng serve 


CLI angular


Angular CLI compila su proyecto e inicia el servidor HTTP de Node. Ahora, puede cargar la aplicación en su navegador apuntándola a la URL <a href="http://localhost:4200"target="_blank"> .

Nuestra primera aplicación angular de CLI

Cuando cargamos la página, vemos la plantilla básica con el nombre del proyecto en la parte superior.


El beneficio de ejecutar "ngserve" es que cualquier cambio realizado en el código hará que el sitio se actualice automáticamente en el navegador, lo que hará que sea mucho más fácil ver que los cambios surtan efecto.


La mayor parte del código en el que nos centraremos se encuentra en el directorio /src/app.


directorio /src/aplicación


app.component.html contiene HTML para el componente que se utiliza actualmente para mostrar la página principal. El código del componente está representado en el archivo app.component.ts (TypeScript).


Eliminaremos el contenido de app.component.html y agregaremos nuestro propio diseño. Crearemos una página dividida que muestre los valores de nuestro proceso de larga duración en el lado izquierdo y dibujaremos algunos círculos aleatorios en el lado derecho. Esto permitirá que nuestro navegador ejecute dos subprocesos de Web Worker adicionales que funcionarán de forma independiente para que pueda ver Angular Web Workers en acción.


Todo el código del resto del artículo se puede obtener del repositorio de GitHub.


Así es como se verá la página final mientras dibuja los círculos aleatorios (antes de que comience el proceso de larga duración).


Acelere su aplicación Angular con Web Workers


Reemplace el código en app.component.html con lo siguiente:


 <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>


La descarga del código también incluye algunos ID y clases CSS y estilos asociados en estilos.css , que se utilizan para el formato muy simple de la interfaz de usuario, por lo que tenemos dos secciones (izquierda y derecha) y otros estilos básicos.


 .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; }


Lo importante a tener en cuenta aquí es que hemos agregado un enlace de evento Angular (clic) al botón. Cuando el usuario hace clic en el botón, el proceso largo se inicia llamando al método longLoop que se encuentra en el archivo TypeScript del componente, app.component.ts .


 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); } } }


Esto ejecuta 10 mil millones de iteraciones escribiendo en una variable miembro de nuestro componente, longProcessOutput.


Debido a que vinculamos esa variable miembro en app.component.html (en el elemento de área de texto), la interfaz de usuario reflejará la actualización cada vez que se actualice la variable. El valor que establecemos en el HTML es donde vinculamos la variable miembro.


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


Ejecutarlo. Veremos que no sucede mucho cuando hacemos clic en el botón y, de repente, el área de texto se actualiza con un montón de valores. Si abrimos la consola, vemos los valores escritos allí mientras se ejecuta el código.

Agregue un componente de círculo aleatorio usando la CLI angular

A continuación, agregaremos un componente de "círculo" para dibujar círculos aleatorios. Podemos hacerlo usando Angular CLI con el siguiente comando:


 ng generate component circle


El comando creó una nueva carpeta llamada círculo y creó cuatro archivos nuevos:

  • círculo.componente.html

  • círculo.component.spec.ts (pruebas unitarias)

  • círculo.component.ts (código TypeScript)

  • círculo.component.css (estilos que solo se aplicarán al HTML asociado para este componente)


Generar círculo de componentes


El HTML es sencillo. Sólo necesitamos el HTML que representará nuestro componente. En nuestro caso, este es el componente en el lado derecho de la página, que mostrará la cuadrícula verde claro y dibujará los círculos. Este dibujo se realiza a través del elemento HTML Canvas.


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


Comenzamos y detenemos el dibujo de los círculos agregando un enlace de evento Angular para capturar el evento del mousedown. Si el usuario hace clic dentro del área del Lienzo en cualquier lugar, los círculos comenzarán a dibujarse si el proceso aún no ha comenzado. Si el proceso ya ha comenzado, entonces el método toggleTimer (que se encuentra en circulo.component.ts ) borra la cuadrícula y detiene el dibujo de círculos.


toggleTimer simplemente usa setInterval para dibujar un círculo en una ubicación aleatoria con un color seleccionado aleatoriamente cada 100 milisegundos (10 círculos/segundo).


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


Hay más código en círculo.component.ts que configura el elemento Canvas, inicializa las variables miembro y realiza el dibujo. Cuando se agrega, su código debería verse así:


 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(); } }


No olvide agregar el componente circular al archivo index.html :


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


Cuando se cargue la página, los círculos comenzarán a dibujarse. Cuando hacemos clic en el botón [Iniciar proceso largo], veremos el dibujo en pausa. Esto se debe a que todo el trabajo se realiza en el mismo hilo.


Solucionemos ese problema agregando un trabajador web.

Agregar un trabajador web angular

Para agregar un nuevo Web Worker usando la CLI, simplemente vamos a la carpeta de nuestro proyecto y ejecutamos el siguiente comando:


 ng generate web-worker app


Ese último parámetro (aplicación) es el nombre del componente que contiene nuestro proceso de larga duración, que querremos colocar en nuestro Web Worker.


Generar aplicación de trabajador web


Angular agregará un código a app.component.ts que se parece al siguiente:


 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. }


¿Qué hace el nuevo código? Podemos ver que este código hace referencia al nuevo componente app.worker que también agregó el comando. En este punto, el código:


  1. Garantiza que el navegador admita Web Workers.
  2. Crea un nuevo trabajador.
  3. Publica un mensaje al trabajador (que se encuentra en app.worker.ts ).
  4. Cuando el trabajador recibe el mensaje de "hola", se activará EventListener (como se muestra en el siguiente fragmento de código).
  5. Cuando se activa EventListener (en app.worker.ts ), creará un objeto de respuesta y se lo enviará a la persona que llama.


Aquí está el contenido completo de app.worker.ts :


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


Veremos mensajes en la consola como resultado de estos pasos, que se verán como la última línea en la siguiente salida de la consola:


Salida de consola


Ese es el console.log que ocurre en el EventHandler que se creó en el objeto Worker original:


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


Eso nos dice que el componente de la aplicación publicó un mensaje al trabajador de la aplicación y el trabajador de la aplicación respondió con un mensaje propio.


Queremos utilizar el Trabajador para ejecutar nuestro proceso de larga duración en otro hilo para que nuestro código de dibujo circular no se interrumpa.


Primero, muevamos el código involucrado con nuestros elementos de UI a un constructor de nuestra clase app.component.


 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. } }


Esto nos permite hacer referencia a la variable longProcessOutput ahora. Con eso podemos acceder a esa variable; tenemos trabajador.onmessage, que agrega los datos básicos al área de texto en lugar de escribirlos en la consola solo como prueba inicial.


Puede ver que el texto resaltado a la izquierda es el mensaje recibido.


Mensaje recibido

LongLoop en el trabajador web

Aún necesitamos mover nuestro bucle de larga duración al Web Worker para asegurarnos de que, cuando se ejecute el bucle, se ejecute en su propio subproceso.


Aquí está la mayor parte del código que tendremos en nuestro app.component.ts final:


 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); } } }


Movimos la variable trabajadora a la clase, que ahora es una variable miembro. De esa manera, podemos hacer referencia a él fácilmente en cualquier lugar de nuestra clase AppComponent.


A continuación, veamos más de cerca cómo hemos definido un controlador de eventos de mensaje en el objeto trabajador con el código en el constructor:


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


Ese código se ejecutará cuando la clase Web Worker (que se encuentra en app.worker.ts ) llame a postMessage(data). Cada vez que se llama al método postMessage, longProcessOutput (el modelo vinculado al área de texto) se actualizará con los datos más un retorno de carro (“\n”), lo cual simplemente hace que cada valor se escriba en su propia línea en el elemento de área de texto.


Aquí está todo el código que se encuentra en el Web Worker real ( app.worker.ts ):


 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); } } });


Este controlador de eventos se activa cuando el usuario hace clic en el botón [Ejecutar proceso largo]. Todo el código que se encuentra en Web Worker ( app.worker.ts ) se ejecuta en un nuevo hilo. Ese es el valor del Web Worker; su código se ejecuta en un hilo separado. Por eso ya no afecta al hilo principal de la aplicación web.


El código de Web Worker se activa cuando el usuario hace clic en el botón porque ahora tenemos el siguiente código en nuestro método longLoop.


 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..."); }


Cuando el mensaje se envía al trabajador, EventListener activa y ejecuta el código de nuestro longLoop original que colocamos allí.

Resumen de aplicaciones angulares con trabajadores web

Cuando ejecute la aplicación, encontrará que hacer clic en el botón [Iniciar proceso largo] ya no hace que el dibujo del círculo se detenga. También podrá interactuar directamente con el componente Canvas de dibujo circular, de modo que si hace clic en él mientras longLoop aún se está ejecutando, el Canvas se volverá a dibujar inmediatamente. Anteriormente, la aplicación se comportaba como si estuviera congelada si hacías esto.


Ahora puede agregar sus procesos de larga duración a Angular Web Worker y aprovechar todos los beneficios de una aplicación que no se congela.


Si desea ver un ejemplo resuelto con JavaScript Web Workers, puede verlo en Plunker .


¿Está buscando componentes de interfaz de usuario independientes del marco? MESCIUS tiene un conjunto completo de componentes de interfaz de usuario de JavaScript, que incluyen cuadrículas de datos, gráficos, indicadores y controles de entrada . También ofrecemos potentes componentes de hojas de cálculo , controles de informes y vistas de presentación mejoradas .


Contamos con un amplio soporte para Angular (así como para React y Vue) y estamos dedicados a ampliar nuestros componentes para su uso en marcos de JavaScript modernos.