Web Workers
Understand how Angular applications use Web Workers to move CPU-intensive calculations away from the browser’s main thread, exchange messages safely, handle errors, provide fallbacks, and keep the user interface responsive.
Lesson content
What is a Web Worker? A Web Worker is a browser feature that runs JavaScript in a background thread separate from the main user-interface thread. Angular applications can use Web Workers to move CPU-intensive calculations away from the main thread so that rendering, scrolling, animations, and user interactions remain responsive. Simple Definition Web Worker = Background JavaScript Thread + CPU-Intensive Work - Direct DOM Access Without a Web Worker Main Thread ↓ Heavy Calculation Starts ↓ UI Rendering Waits ↓ Buttons and Scrolling May Freeze ↓ Calculation Completes ↓ UI Responds Again With a Web Worker Main Thread │ ├── UI Rendering ├── User Events └── Sends Work to Worker ↓ Worker Thread ↓ Heavy Calculation ↓ Sends Result Back ↓ Main Thread Updates UI Why Use Web Workers? Keep the user interface responsive Move CPU-heavy work off the main thread Reduce long-running main-thread tasks Improve scrolling and animation responsiveness Process large datasets without blocking user input Run suitable calculations in parallel with UI work Suitable Web Worker Tasks Web Workers are useful for computationally expensive work such as: Processing large arrays Sorting or filtering very large datasets Image or pixel processing Generating reports Parsing large files Data compression Encryption and hashing Geometric calculations Scientific calculations Preparing chart data Tasks That Usually Do Not Need a Worker Simple button-click handlers Small array transformations Normal Angular template updates Basic form validation Ordinary HTTP requests Small signal or computed calculations HTTP requests are already asynchronous. A Web Worker is mainly useful when JavaScript computation itself blocks the main thread. Create a Web Worker with Angular CLI Use the Angular CLI web-worker schematic. ng generate web-worker workers/report Short form: ng g web-worker workers/report The CLI creates and configures a worker file such as: src/app/workers/report.worker.ts Basic Worker File addEventListener( 'message', ({ data }) => { const result = data * data; postMessage(result); } ); Worker Flow Main Thread Sends Data ↓ Worker Receives message Event ↓ Worker Performs Calculation ↓ Worker Calls postMessage() ↓ Main Thread Receives Result Create and Use the Worker import { Component, signal } from '@angular/core'; @Component({ selector: 'app-calculator', template: ` <button (click)="calculate()" > Calculate </button> @if (result() !== null) { <p> Result: {{ result() }} </p> } ` }) export class CalculatorComponent { result = signal<number | null>( null ); calculate() { if ( typeof Worker === 'undefined' ) { this.result.set( this.calculateLocally(10) ); return; } const worker = new Worker( new URL( './workers/report.worker', import.meta.url ) ); worker.onmessage = ({ data }) => { this.result.set(data); worker.terminate(); }; worker.onerror = error => { console.error( 'Worker error:', error ); worker.terminate(); }; worker.postMessage(10); } private calculateLocally( value: number ): number { return value * value; } } Important Worker Creation Syntax new Worker( new URL( './workers/report.worker', import.meta.url ) ); This syntax allows the build system to locate, compile, and bundle the worker separately. Typed Worker Messages Use interfaces or union types to keep communication between the main thread and worker strongly typed. Message Types export interface ReportRequest { type: 'generate-report'; values: number[]; } export interface ReportSuccess { type: 'success'; total: number; average: number; } export interface ReportFailure { type: 'error'; message: string; } export type ReportResponse = ReportSuccess | ReportFailure; Worker Implementation addEventListener( 'message', ({ data }: MessageEvent<ReportRequest>) => { try { const total = data.values.reduce( (sum, value) => sum + value, 0 ); const average = data.values.length ? total / data.values.length : 0; const response: ReportSuccess = { type: 'success', total, average }; postMessage(response); } catch (error) { const response: ReportFailure = { type: 'error', message: error instanceof Error ? error.message : 'Unknown worker error' }; postMessage(response); } } ); Component Usage worker.onmessage = ({ data }: MessageEvent<ReportResponse>) => { if (data.type === 'success') { console.log( data.total, data.average ); } else { console.error( data.message ); } }; worker.postMessage({ type: 'generate-report', values: [ 10, 20, 30 ] } satisfies ReportRequest); Structured Clone Data sent through postMessage() is normally copied using the browser's structured-clone algorithm. Common supported values include: Strings Numbers Booleans Arrays Plain objects Dates Maps and Sets ArrayBuffer Typed arrays Functions and DOM elements cannot be sent as normal worker messages. Invalid Message Examples worker.postMessage({ calculate: () => 10 }); worker.postMessage( document.body ); Transferable Objects Large binary data can be transferred instead of copied. Transferable objects are useful for values such as ArrayBuffer . const buffer = new ArrayBuffer( 1024 * 1024 ); worker.postMessage( { buffer }, [ buffer ] ); After transfer, ownership moves to the worker and the original buffer can no longer be used normally by the sender. Copy vs Transfer Copy Transfer Creates a cloned value Moves ownership Sender keeps its original value Sender loses access to the transferred resource Can be expensive for large binary data Can avoid expensive memory copying Complete Large-Calculation Example Worker File export interface PrimeRequest { maximum: number; } export interface PrimeResponse { primes: number[]; duration: number; } addEventListener( 'message', ({ data }: MessageEvent<PrimeRequest>) => { const startedAt = performance.now(); const primes: number[] = []; for ( let value = 2; value <= data.maximum; value++ ) { if (isPrime(value)) { primes.push(value); } } const response: PrimeResponse = { primes, duration: performance.now() - startedAt }; postMessage(response); } ); function isPrime( value: number ): boolean { if (value < 2) { return false; } for ( let divisor = 2; divisor <= Math.sqrt(value); divisor++ ) { if ( value % divisor === 0 ) { return false; } } return true; } Component import { Component, OnDestroy, signal } from '@angular/core'; @Component({ selector: 'app-prime-calculator', template: ` <button (click)="start()" [disabled]="isProcessing()" > Find Prime Numbers </button> <button (click)="cancel()" [disabled]="!isProcessing()" > Cancel </button> @if (isProcessing()) { <p>Processing...</p> } @if (duration() !== null) { <p> Found {{ totalPrimes() }} prime numbers in {{ duration() }} ms. </p> } @if (errorMessage()) { <p> {{ errorMessage() }} </p> } ` }) export class PrimeCalculatorComponent implements OnDestroy { isProcessing = signal(false); totalPrimes = signal(0); duration = signal<number | null>( null ); errorMessage = signal(''); private worker: Worker | null = null; start() { this.cancel(); this.isProcessing.set(true); this.errorMessage.set(''); this.duration.set(null); if ( typeof Worker === 'undefined' ) { this.isProcessing.set(false); this.errorMessage.set( 'Web Workers are unavailable.' ); return; } this.worker = new Worker( new URL( './prime.worker', import.meta.url ) ); this.worker.onmessage = ({ data }: MessageEvent<PrimeResponse>) => { this.totalPrimes.set( data.primes.length ); this.duration.set( Math.round(data.duration) ); this.isProcessing.set(false); this.destroyWorker(); }; this.worker.onerror = error => { this.errorMessage.set( error.message || 'Worker calculation failed.' ); this.isProcessing.set(false); this.destroyWorker(); }; this.worker.postMessage({ maximum: 500000 } satisfies PrimeRequest); } cancel() { this.destroyWorker(); this.isProcessing.set(false); } ngOnDestroy() { this.destroyWorker(); } private destroyWorker() { this.worker?.terminate(); this.worker = null; } } Worker Lifecycle Create Worker ↓ Register Message and Error Handlers ↓ Send Task ↓ Receive Result ↓ Reuse Worker or Terminate It ↓ Clean Up When Component Is Destroyed terminate() Use terminate() to immediately stop a worker. worker.terminate(); Termination stops the worker without waiting for its current operation to complete. Use it when: The component is destroyed The user cancels the operation The worker encounters an unrecoverable error A one-time calculation finishes Closing a Worker from Inside A dedicated worker can stop itself using close() . postMessage(result); close(); Use this mainly for one-time workers. Do not close a worker that should process future tasks. Reusable Worker Service Worker creation and message handling can be moved into a service when multiple components need the same functionality. import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class ReportWorkerService { run( request: ReportRequest ): Promise<ReportResponse> { return new Promise( ( resolve, reject ) => { if ( typeof Worker === 'undefined' ) { reject( new Error( 'Web Workers are unavailable.' ) ); return; } const worker = new Worker( new URL( './report.worker', import.meta.url ) ); worker.onmessage = ({ data }: MessageEvent<ReportResponse>) => { resolve(data); worker.terminate(); }; worker.onerror = error => { reject( new Error( error.message ) ); worker.terminate(); }; worker.postMessage( request ); } ); } } Use the Worker Service export class ReportComponent { private reportWorker = inject( ReportWorkerService ); async generateReport() { try { const result = await this.reportWorker.run({ type: 'generate-report', values: [ 10, 20, 30 ] }); console.log(result); } catch (error) { console.error(error); } } } Observable-Based Worker Service An Observable can represent worker progress, results, errors, and cancellation. import { Observable } from 'rxjs'; runCalculation( request: CalculationRequest ): Observable<CalculationResponse> { return new Observable( subscriber => { if ( typeof Worker === 'undefined' ) { subscriber.error( new Error( 'Web Workers are unavailable.' ) ); return; } const worker = new Worker( new URL( './calculation.worker', import.meta.url ) ); worker.onmessage = ({ data }: MessageEvent<CalculationResponse>) => { subscriber.next(data); subscriber.complete(); }; worker.onerror = error => { subscriber.error(error); }; worker.postMessage(request); return () => { worker.terminate(); }; } ); } Unsubscribing from this Observable terminates the worker. Reporting Progress A worker can send multiple messages during a long-running task. Worker interface ProgressResponse { type: 'progress'; percentage: number; } interface CompleteResponse { type: 'complete'; result: number; } for ( let index = 0; index < totalItems; index++ ) { processItem(index); if (index % 100 === 0) { postMessage({ type: 'progress', percentage: Math.round( ( index / totalItems ) * 100 ) } satisfies ProgressResponse); } } postMessage({ type: 'complete', result } satisfies CompleteResponse); Main Thread worker.onmessage = ({ data }) => { if ( data.type === 'progress' ) { this.progress.set( data.percentage ); return; } if ( data.type === 'complete' ) { this.result.set( data.result ); worker.terminate(); } }; Cancelling Work Cooperatively Calling terminate() stops the entire worker immediately. For a reusable worker, the main thread can instead send a cancellation message and allow the worker to stop its current task cleanly. Main Thread worker.postMessage({ type: 'cancel', taskId }); Worker const cancelledTasks = new Set<string>(); addEventListener( 'message', ({ data }) => { if (data.type === 'cancel') { cancelledTasks.add( data.taskId ); return; } if (data.type === 'start') { processTask(data); } } ); Web Worker Limitations A worker runs in a separate global environment and cannot directly use many browser and Angular UI APIs. Available Unavailable Directly JavaScript calculations DOM elements postMessage() document fetch() window Timers Angular templates Web APIs supported in workers Direct component updates Imported pure TypeScript functions Normal component dependency injection context Invalid Worker Code document .querySelector('button') ?.classList.add('active'); this.componentSignal.set(10); The worker must send a result to the main thread, and the Angular component updates the user interface. Using Pure Shared Functions Move reusable calculation logic into pure functions that can be used by both the worker and a fallback implementation. Shared Calculation export function calculateTotal( values: readonly number[] ): number { return values.reduce( (total, value) => total + value, 0 ); } Worker import { calculateTotal } from './report-calculation'; addEventListener( 'message', ({ data }: MessageEvent<number[]>) => { postMessage( calculateTotal(data) ); } ); Fallback if ( typeof Worker === 'undefined' ) { return calculateTotal(values); } Fallback Support Some environments do not support Web Workers. Always check before creating one: if ( typeof Worker !== 'undefined' ) { // Use worker } else { // Use fallback } A fallback is especially important for server-side rendering, testing environments, and unsupported browsers or platforms. Web Workers and Server-Side Rendering Server-side rendering does not provide the browser's normal Worker environment. Avoid creating a worker during server rendering. if ( typeof Worker === 'undefined' ) { return this.calculateLocally( request ); } Create the worker only in browser-executed code and provide a non-worker fallback. Web Worker vs Service Worker Web Worker Service Worker Runs CPU-intensive JavaScript Works as a network and caching layer Created by a page or application Registered independently with the browser Communicates through messages Intercepts supported network requests Usually ends with its page or termination Can continue across page sessions Used for background computation Used for offline caching and PWA behavior Easy Difference Web Worker = Background Computation Service Worker = Network Proxy and Cache Web Worker vs Asynchronous Function Async Function Web Worker Usually runs JavaScript on the main thread Runs JavaScript on another thread Useful for waiting on I/O Useful for CPU-intensive processing Does not automatically prevent CPU blocking Can keep CPU-heavy work away from the UI thread async/await Worker and postMessage() Important Example async function calculate() { performHeavyLoop(); } Adding async does not automatically move the heavy loop to another thread. Web Worker vs setTimeout() Using setTimeout() delays work, but the callback still runs on the main thread. setTimeout( () => { performHeavyCalculation(); }, 0 ); This may allow the browser to render first, but the heavy calculation can still block the UI when it starts. setTimeout() = Delayed Main-Thread Work Web Worker = Separate-Thread Work Worker Pool Applications with many independent tasks may use a small pool of reusable workers. Pending Tasks ↓ Worker Pool ┌────┼────┐ ↓ ↓ ↓ W1 W2 W3 Do not create an unlimited number of workers. Every worker consumes memory and system resources. Use a worker pool only after measuring a genuine need. Performance Considerations A Web Worker has overhead: Worker creation Loading the worker bundle Copying or transferring messages Serializing large data structures Managing worker lifecycle For small calculations, this overhead can be greater than the performance benefit. Performance Formula Worker Benefit = Main-Thread Time Saved - Worker Setup - Message Transfer - Coordination Cost When to Reuse a Worker Reuse a worker when: The same feature sends frequent tasks Worker startup is relatively expensive The worker maintains useful internal calculation state Multiple sequential operations are expected Create a one-time worker when: The operation is rare The worker is feature-specific Cleanup and isolation are more important than startup cost Error Handling Worker Runtime Error worker.onerror = event => { console.error( event.message ); worker.terminate(); }; Message Deserialization Error worker.onmessageerror = event => { console.error( 'Unable to read worker message', event ); }; Application-Level Error Message postMessage({ type: 'error', message: 'Unable to process report' }); Use both worker event handlers and typed application-level error responses. Testing Worker Logic Keep the heavy calculation in a pure function so it can be tested without starting a real worker. export function calculateAverage( values: readonly number[] ): number { if (!values.length) { return 0; } const total = values.reduce( (sum, value) => sum + value, 0 ); return total / values.length; } Unit Test describe( 'calculateAverage', () => { it( 'calculates an average', () => { expect( calculateAverage([ 10, 20, 30 ]) ).toBe(20); } ); it( 'returns zero for an empty list', () => { expect( calculateAverage([]) ).toBe(0); } ); } ); Integration tests can separately verify worker creation and message handling. Common Mistakes 1. Using a Worker for Small Calculations Worker startup and message-transfer overhead may make small tasks slower. 2. Trying to Access the DOM Avoid inside a worker: document.getElementById( 'result' ); Return the result to the Angular component and update the UI there. 3. Sending Functions or Component Instances Worker messages should contain serializable data, not Angular component instances, services, functions, or DOM elements. 4. Forgetting to Terminate One-Time Workers Terminate workers when they are no longer required. 5. Creating a Worker During Server Rendering Check whether Worker exists and provide a fallback. 6. Sending Very Large Objects Repeatedly Repeated copying can become expensive. Send only required data or use transferable objects where appropriate. 7. Assuming async/await Uses Another Thread Asynchronous syntax does not automatically move CPU-heavy JavaScript away from the main thread. 8. Creating Too Many Workers Workers consume memory and CPU resources. Use a limited number based on measured requirements. 9. Mixing UI and Calculation Logic Keep worker logic independent from Angular components and templates. 10. Ignoring Worker Errors Handle onerror , application error responses, cancellation, and cleanup. Best Practices Use workers only for measured CPU-intensive tasks Keep worker code independent from UI code Use typed request and response messages Send only the data required for the calculation Use transferable objects for large binary data when appropriate Provide a fallback for unsupported and server-rendered environments Terminate one-time workers after completion or failure Clean up workers when their owning feature is destroyed Support cancellation for long-running work Report progress for operations that take noticeable time Extract calculations into pure, testable functions Measure performance before and after adding a worker Avoid creating an excessive number of workers Interview Questions What is a Web Worker? A Web Worker runs JavaScript on a background thread so CPU-intensive work does not block the browser's main user-interface thread. How do you generate a worker in Angular? ng generate web-worker workers/report How does the main thread communicate with a worker? Both sides exchange messages using postMessage() and listen for message events. Can a Web Worker access the DOM? No. A worker cannot directly access document , DOM elements, or Angular component views. Does async/await move work to another thread? No. CPU-intensive JavaScript still runs on the main thread unless it is explicitly moved to a worker or another execution environment. When should a Web Worker be used? Use it when measured CPU-intensive processing causes noticeable main-thread blocking or UI responsiveness problems. What is the difference between a Web Worker and a Service Worker? A Web Worker performs background computation. A Service Worker primarily manages network requests, caching, and offline application behavior. What is structured cloning? Structured cloning is the browser mechanism normally used to copy supported data between the main thread and a worker. What is a transferable object? A transferable object moves ownership of supported data, such as an ArrayBuffer , instead of copying it. How do you stop a worker? The main thread can call worker.terminate() , while the worker can call close() . How do Web Workers work with server-side rendering? Workers are browser APIs and are not normally available during server rendering. Check for support and provide a fallback implementation. Does a Web Worker always improve performance? No. Worker creation and message transfer have costs. Workers are beneficial mainly when the computation saved on the main thread is greater than those costs. Key Point A Web Worker keeps the Angular user interface responsive by moving suitable CPU-intensive JavaScript calculations to a separate browser thread. Easy Formula Main Thread = Angular UI + Rendering + User Events Web Worker = Background Calculation Communication = postMessage() + message Event Large Binary Data = Transferable Object Cleanup = terminate() Unsupported Environment = Fallback Main Thread ↓ Create Worker ↓ Send Typed Request ↓ Worker Calculates ↓ Send Progress or Result ↓ Angular Updates State ↓ Terminate or Reuse Worker