Web Workers in Angular: Multithreading without headache

Marco Pollacci 16/04/2024 - BolognaJS
Hello Folks!
me Marco Pollacci Senior Frontend Developer @GELLIFY
qr

Your Yearly Shot of Angular at

17/04/2024: Angular e Signalqrcode_angular-signal.pages.dev Today: Angular e Web Worker

What is Multithreading?

octo multitasking

In computer architecture, multithreading is the ability of a central processing unit (CPU) (or a single core in a multi-core processor) to provide multiple threads of execution.

https://en.wikipedia.org/wiki/Multithreading_(computer_architecture)

And what about thread management in

Web Browsers?

๐Ÿค”

Web browsers are complex applications that handle a variety of tasks simultaneously

๐Ÿ‘‰ Rendering web pages ๐Ÿ‘‰ Executing JavaScript ๐Ÿ‘‰ Managing network requests ๐Ÿ‘‰ Handling user input

Single-Threaded ๐Ÿ›ฃ

๐Ÿ“ JavaScript Execution ๐Ÿ“ Event Handling ๐Ÿ“ Rendering ๐Ÿ“ DOM Manipulation

Multi-Threaded ๐Ÿ›ฃ ๐Ÿ›ฃ

๐Ÿš€ Networking ๐Ÿš€ GPU Acceleration ๐Ÿš€ HTML5 APIs (WebRTC, Web Audio API, File API) ๐Ÿš€ Web worker

๐ŸŒฐ In a nutshell

Web browsers employ a hybrid approach to thread management.

๐Ÿ“ฃ Single-Threaded: JavaScript execution on the main thread, managed by an event loop.

๐Ÿ“ฃ Multi-Threaded: Use of Web Workers for background tasks, and multi-threaded browser engine components for rendering, networking, and GPU acceleration.

The Freezing problem

freezing man

The Event Loop

event loop credits: Arslan Younis

BUT

The Synchronous code

     
      const heavyTask = () => {
        for (let i = 0; i < 10000000000; i++) {
          //do something useful
        }
      };
   

block the call stack!

problem solution meme spongbob

โš™๏ธ Web Workers โš™๏ธ

Let's see how it works

๐Ÿ‘€

Let's Code ๐Ÿ‘จโ€๐Ÿ’ป

  
  self.onmessage = (event) => {
    console.log("Worker received:", event.data);
    // Do heavy stuff
    self.postMessage(`Hello from worker!`);
  };

Let's Code ๐Ÿ‘จโ€๐Ÿ’ป

    
  const worker = new Worker("worker.js");

  worker.onmessage = (event) => {
    console.log("Main thread received:", event.data);
    // Do some other stuff
   };
  
   // Starting point
   worker.postMessage("Hello Web Day!");
  

โ—(แต”แ—œแต”)โ—œ

console log worker
sofar sogood meme

Let's go further แฏ“๐Ÿƒ๐Ÿปโ€โ™€๏ธโ€โžก๏ธ

self.onmessage = (event) => {}; self.onmessage = (event) => {}; postMessage(message, transfer) postMessage(message, transfer)

๐Ÿ’ฌ message

๐Ÿ“จ will be in the data field in the message object

๐Ÿšฉ required

๐Ÿšš transfer

๐ŸšŒ only transferable objects like ArrayBuffer or ImageBitmap

๐Ÿ˜ถโ€๐ŸŒซ๏ธ optional

worker.onerror = (event) => {}; worker.onerror = (event) => {};

๐Ÿค๐Ÿป Web Worker - Practical Use Cases

Let's go further แฏ“แฏ“๐Ÿƒ๐Ÿปโ€โ™€๏ธโ€โžก๏ธ๐Ÿƒ๐Ÿปโ€โ™€๏ธโ€โžก๏ธ

๐Ÿค๐Ÿป Shared Worker

๐Ÿค๐Ÿป Shared Worker

๐Ÿค๐Ÿป Shared Worker - Practical Use Cases

Let's Code ๐Ÿ‘จโ€๐Ÿ’ป

  
    const clients = [];
    
    self.onconnect = (event) => {
      const [port] = event.ports;
      clients.push(port);
      port.postMessage("Worker connected");
      port.onmessage = (e) => {
        clients.forEach((client) =>
          client.postMessage(
            `New client connected with message: "${e.data}"`
          )
        );
      };
    };

Let's Code ๐Ÿ‘จโ€๐Ÿ’ป

    
    const sharedWorker = new SharedWorker("shared-worker.js");
    sharedWorker.port.start();

    sharedWorker.port.onmessage = (e) => {
      console.log(e.data);
    };

    sharedWorker.port.postMessage("Hello from Tab 1");
  

โ›“๏ธโ€๐Ÿ’ฅ when client disconnect

Let's Code ๐Ÿ‘จโ€๐Ÿ’ป

    
  const clients = [];
  self.onconnect = (event) => {
    // ...other stuff
    port.onclose = () => {
      clients.splice(clients.indexOf(port), 1);
      clients.forEach((client) =>
        client.postMessage(
          `Client disconnected. Total clients remaining: ${clients.length}`
        )
      );
    };
  }

Web Worker limitations โŒ

angular logo

Angular Web Workers

Angular CLI allows us to generate new Web Worker

โš™๏ธ Angular CLI

โš™๏ธ Angular CLI

    
      ng g web-worker services/worker-test
      
      CREATE src/app/services/worker-test.worker.ts (157 bytes)
      CREATE tsconfig.worker.json (334 bytes)
      UPDATE angular.json (2946 bytes)
      UPDATE src/app/services/worker-test.service.ts (537 bytes)
    
    
      /// <reference lib="webworker" />

      addEventListener('message', ({ data }) => {
        const response = `worker response to ${data}`;
        postMessage(response);
      });

    
    
    
      // ...
      export class WorkerTestService {
        constructor() { }
      }
      
    if (typeof Worker !== 'undefined') {
        // Create a new
        const worker = new Worker(new URL('./worker-test.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.
      }
    

Let's see some example code!

omg

Let's Code ๐Ÿ‘จโ€๐Ÿ’ป

    
      addEventListener('message', ({ data }) => {
        heavyTask();
        postMessage(`done "${data}" task`);
      });
    
  

Let's Code ๐Ÿ‘จโ€๐Ÿ’ป

    
      //...
      export class WorkerTestService {
        #worker!: Worker;
        
        constructor() {
          this.createWorker();
        }
      
        createWorker() {
          this.#worker = new Worker(
            new URL('./worker-test.worker.ts', import.meta.url)
          );
        }
      
      doHeavyWork(data: string): Observable<string> {
          return new Observable((observer) => {
            this.#worker.onmessage = ({ data }: MessageEvent<string>) => {
              observer.next(data);
              observer.complete();
            };
            this.#worker.onerror = (error) => observer.error(error);
            this.#worker.postMessage(data);
          });
        }
      }
    

Let's Code ๐Ÿ‘จโ€๐Ÿ’ป

    
      // ...
      export class HomeComponent {
        readonly #workerService = inject(WorkerTestService);
        
        startHeavyWorkInWorker() {
          this.#workerService
            .doHeavyWork('daje')
            .subscribe((data) => console.log(data)); // done "daje" task
        }
      }
  

Web Worker Pool

๐Ÿ“š

๐Ÿ“š Web Worker Pool

How Does a Web Worker Pool Work?

( ๊ฉœ แฏ… ๊ฉœ;)

Pool worker loop ๐Ÿ”

Pool Initialization Task Assignment Worker Release

Let's Code ๐Ÿ‘จโ€๐Ÿ’ป

    
  const POOL_SIZE = 4;
  export class WorkerPoolService {
    #workers: Worker[] = [];

    constructor() {
      this.createPoolWorkers();
    }
    createPoolWorkers() {
        for (let i = 0; i < POOL_SIZE; i++) {
          const worker = new Worker(
            new URL('./worker-test.worker.ts', import.meta.url)
          );
          this.#increaseWorkerPool(worker);
        }
    }
    #increaseWorkerPool(worker: Worker) {
      this.#workers.push(worker);
    }
  }
    

Let's Code ๐Ÿ‘จโ€๐Ÿ’ป

    
    export class WorkerPoolService {
      //...
      getWorker(data: string): Observable<string> | null {
        const worker = this.#workers.pop();
        if (!worker) return null;
        return new Observable((observer) => {
          worker.onmessage = ({ data }: MessageEvent<string>) => {
            observer.next(data);
            observer.complete();
            this.#increaseWorkerPool(worker);
          };
          worker.onerror = (error) => observer.error(error);
          worker.postMessage(data);
        });
      }
    }

Let's Code ๐Ÿ‘จโ€๐Ÿ’ป

    
      export class PoolWorkerComponent {     
        //...
        startNewHeavyJob(task: string) {
          const newWorker = this.#poolWorkerService.getWorker(task);
          if (!newWorker) {
            this.disableButton.set(true);
            return;
          }
          newWorker.subscribe((data) => {
            this.disableButton.set(false);
            console.log(data);
          });
        }
      }

๐Ÿ™…โ€โ™‚๏ธ Common Pitfalls

What Could Go Wrong? ๐ŸŒš

thatsall

You can see this slide on

qrcode https://talk-web-workers-angular.pages.dev/bologna-js

You can see the code on github

qrcode https://github.com/marcopollacci/web-worker-angular-example
Questions? ๐Ÿค”
     
      self.onmessage = async (event) => {
        const { data: question } = event;
        
        const answer = await answer(question);
        self.postMessage(`The answer is: ${answer}`);
      };
    
   
Thank you again!
me Marco Pollacci Senior Frontend Developer @GELLIFY
Leave some feedback
qr
coming soon