阿里云主机折上折
  • 微信号
Current Site:Index > with Web Workers

with Web Workers

Author:Chuan Chen 阅读数:13825人阅读 分类: TypeScript

Introduction to Web Workers

Web Workers allow scripts to run in background threads, preventing the main thread from being blocked. TypeScript provides type support, making the use of Web Workers safer and more efficient. By offloading computationally intensive tasks to Workers, page responsiveness can be significantly improved.

Creating a Web Worker

Creating a Web Worker in TypeScript requires two files: the main thread script and the Worker script. First, create the Worker script:

// worker.ts
self.onmessage = (e: MessageEvent) => {
  const data = e.data;
  // Process data
  const result = heavyCalculation(data);
  self.postMessage(result);
};

function heavyCalculation(input: number[]): number {
  return input.reduce((a, b) => a + b, 0);
}

Then, create a Worker instance in the main thread:

// main.ts
const worker = new Worker(new URL('./worker.ts', import.meta.url), {
  type: 'module'
});

worker.onmessage = (e: MessageEvent) => {
  console.log('Result from Worker:', e.data);
};

worker.postMessage([1, 2, 3, 4, 5]);

Type-Safe Worker Communication

To ensure type safety, communication interfaces can be defined:

// worker-types.ts
interface WorkerRequest {
  type: 'calculate';
  data: number[];
}

interface WorkerResponse {
  type: 'result';
  data: number;
}

Modify the Worker and main thread code to use these types:

// worker.ts
self.onmessage = (e: MessageEvent<WorkerRequest>) => {
  if (e.data.type === 'calculate') {
    const result = heavyCalculation(e.data.data);
    self.postMessage({
      type: 'result',
      data: result
    } as WorkerResponse);
  }
};

Advanced Worker Patterns

Worker Pool

For scenarios requiring large-scale task processing, a Worker pool can be created:

class WorkerPool {
  private workers: Worker[] = [];
  private taskQueue: Array<{
    task: WorkerRequest;
    resolve: (value: WorkerResponse) => void;
  }> = [];
  
  constructor(size: number) {
    for (let i = 0; i < size; i++) {
      const worker = new Worker(new URL('./worker.ts', import.meta.url), {
        type: 'module'
      });
      
      worker.onmessage = (e: MessageEvent<WorkerResponse>) => {
        const nextTask = this.taskQueue.shift();
        if (nextTask) {
          worker.postMessage(nextTask.task);
          nextTask.resolve(e.data);
        }
      };
      
      this.workers.push(worker);
    }
  }
  
  execute(task: WorkerRequest): Promise<WorkerResponse> {
    return new Promise((resolve) => {
      this.taskQueue.push({ task, resolve });
    });
  }
}

SharedArrayBuffer and Atomics

For scenarios requiring shared memory:

// Main thread
const sharedBuffer = new SharedArrayBuffer(1024);
const sharedArray = new Int32Array(sharedBuffer);

const worker = new Worker(new URL('./shared-worker.ts', import.meta.url));
worker.postMessage({ buffer: sharedBuffer });

// shared-worker.ts
self.onmessage = (e: MessageEvent<{ buffer: SharedArrayBuffer }>) => {
  const sharedArray = new Int32Array(e.data.buffer);
  Atomics.add(sharedArray, 0, 1);
  self.postMessage({ done: true });
};

Error Handling in Workers

Properly handle errors in Workers:

// worker.ts
try {
  // Code that might fail
  self.postMessage({ result: heavyCalculation(data) });
} catch (error) {
  self.postMessage({ 
    error: true,
    message: error instanceof Error ? error.message : 'Unknown error'
  });
}

// Main thread
worker.onmessage = (e: MessageEvent<{ result?: number; error?: boolean; message?: string }>) => {
  if (e.data.error) {
    console.error('Worker error:', e.data.message);
  } else {
    console.log('Result:', e.data.result);
  }
};

worker.onerror = (e: ErrorEvent) => {
  console.error('Worker runtime error:', e.message);
};

Modularizing Workers

Organize Worker code using ES modules:

// math-utils.ts
export function calculateSum(numbers: number[]): number {
  return numbers.reduce((a, b) => a + b, 0);
}

// worker.ts
import { calculateSum } from './math-utils';

self.onmessage = (e: MessageEvent<number[]>) => {
  const result = calculateSum(e.data);
  self.postMessage(result);
};

Worker Lifecycle Management

Properly manage Worker lifecycle:

class TaskProcessor {
  private worker: Worker | null = null;
  
  start() {
    if (!this.worker) {
      this.worker = new Worker(new URL('./worker.ts', import.meta.url));
      this.worker.onmessage = this.handleMessage;
    }
  }
  
  stop() {
    if (this.worker) {
      this.worker.terminate();
      this.worker = null;
    }
  }
  
  private handleMessage = (e: MessageEvent) => {
    // Handle messages
  };
  
  postTask(data: unknown) {
    if (this.worker) {
      this.worker.postMessage(data);
    }
  }
}

Limitations of Web Workers

Understanding Worker limitations is important:

  1. Cannot directly access the DOM
  2. Cannot use certain APIs (e.g., localStorage)
  3. Communication data is copied via the structured clone algorithm

Performance Optimization Tips

Methods to optimize Worker performance:

  1. Batch process messages
  2. Use Transferable objects
  3. Set an appropriate number of Workers
// Using Transferable objects
const largeBuffer = new ArrayBuffer(10000000);
worker.postMessage(largeBuffer, [largeBuffer]);

// Batch processing
worker.postMessage({
  tasks: [task1, task2, task3],
  batchId: 'batch-123'
});

Coordination Between Workers and Main Thread

Implement complex coordination logic:

// Main thread
const worker = new Worker(new URL('./coordinated-worker.ts', import.meta.url));

worker.onmessage = (e: MessageEvent<{ type: string; data: unknown }>) => {
  switch (e.data.type) {
    case 'progress':
      updateProgressBar(e.data.data as number);
      break;
    case 'result':
      displayResult(e.data.data);
      break;
    case 'error':
      showError(e.data.data as string);
      break;
  }
};

// coordinated-worker.ts
function processInChunks(data: LargeDataSet, chunkSize: number) {
  for (let i = 0; i < data.length; i += chunkSize) {
    const chunk = data.slice(i, i + chunkSize);
    const result = processChunk(chunk);
    
    self.postMessage({
      type: 'progress',
      data: (i + chunkSize) / data.length
    });
    
    self.postMessage({
      type: 'partial-result',
      data: result
    });
  }
}

Advanced TypeScript Types in Workers

Leverage TypeScript's advanced type features:

// Define message type mappings
type WorkerMessages = {
  calculate: { numbers: number[] };
  fetch: { url: string };
  cancel: null;
};

type WorkerResponses = {
  result: { value: number };
  data: { content: string };
  error: { message: string };
};

// Type-safe Worker wrapper
class TypedWorker {
  private worker: Worker;
  
  constructor() {
    this.worker = new Worker(new URL('./typed-worker.ts', import.meta.url));
  }
  
  postMessage<T extends keyof WorkerMessages>(
    type: T,
    data: WorkerMessages[T]
  ): Promise<WorkerResponses[keyof WorkerResponses]> {
    return new Promise((resolve, reject) => {
      const messageId = Math.random().toString(36).slice(2);
      
      const handler = (e: MessageEvent<{
        messageId: string;
        type: keyof WorkerResponses;
        data: WorkerResponses[keyof WorkerResponses];
      }>) => {
        if (e.data.messageId === messageId) {
          this.worker.removeEventListener('message', handler);
          if (e.data.type === 'error') {
            reject(e.data.data);
          } else {
            resolve(e.data.data);
          }
        }
      };
      
      this.worker.addEventListener('message', handler);
      this.worker.postMessage({ type, data, messageId });
    });
  }
}

State Management in Workers

Manage complex state in Workers:

// state-worker.ts
interface State {
  counter: number;
  data: Record<string, unknown>;
  processing: boolean;
}

let state: State = {
  counter: 0,
  data: {},
  processing: false
};

self.onmessage = (e: MessageEvent<{ type: string; payload?: unknown }>) => {
  switch (e.data.type) {
    case 'increment':
      state.counter++;
      self.postMessage({ type: 'state-update', state });
      break;
      
    case 'load-data':
      state.processing = true;
      loadDataAsync(e.data.payload as string).then(data => {
        state.data = data;
        state.processing = false;
        self.postMessage({ type: 'data-loaded', data });
      });
      break;
      
    case 'get-state':
      self.postMessage({ type: 'current-state', state });
      break;
  }
};

async function loadDataAsync(url: string): Promise<Record<string, unknown>> {
  // Simulate async data loading
  return new Promise(resolve => {
    setTimeout(() => resolve({ url, timestamp: Date.now() }), 1000);
  });
}

Integrating Web Workers in Modern Frameworks

Example of using Workers in React:

// useWorker.ts
import { useEffect, useRef, useState } from 'react';

export function useWorker<T, R>(workerUrl: string) {
  const [result, setResult] = useState<R | null>(null);
  const [error, setError] = useState<Error | null>(null);
  const workerRef = useRef<Worker | null>(null);
  
  useEffect(() => {
    const worker = new Worker(new URL(workerUrl, import.meta.url));
    workerRef.current = worker;
    
    worker.onmessage = (e: MessageEvent<R>) => {
      setResult(e.data);
    };
    
    worker.onerror = (e) => {
      setError(new Error(e.message));
    };
    
    return () => {
      worker.terminate();
    };
  }, [workerUrl]);
  
  const postMessage = (data: T) => {
    if (workerRef.current) {
      workerRef.current.postMessage(data);
    }
  };
  
  return { result, error, postMessage };
}

// Usage in component
function CalculationComponent() {
  const { result, error, postMessage } = useWorker<number[], number>('./worker.ts');
  
  const handleCalculate = () => {
    postMessage([1, 2, 3, 4, 5]);
  };
  
  return (
    <div>
      <button onClick={handleCalculate}>Calculate</button>
      {error && <div>Error: {error.message}</div>}
      {result !== null && <div>Result: {result}</div>}
    </div>
  );
}

Complex Calculation Example in Workers

Implementing an image processing Worker:

// image-worker.ts
interface ImageTask {
  id: string;
  imageData: ImageData;
  operations: ('grayscale' | 'invert' | 'blur')[];
}

self.onmessage = (e: MessageEvent<ImageTask>) => {
  const { id, imageData, operations } = e.data;
  const result = processImage(imageData, operations);
  
  self.postMessage({
    id,
    result
  }, [result.data.buffer]); // Transfer image data using Transferable
};

function processImage(imageData: ImageData, operations: string[]): ImageData {
  const { width, height, data } = imageData;
  const result = new ImageData(width, height);
  const resultData = result.data;
  
  // Copy original data
  for (let i = 0; i < data.length; i++) {
    resultData[i] = data[i];
  }
  
  // Apply each operation
  operations.forEach(op => {
    switch (op) {
      case 'grayscale':
        applyGrayscale(resultData);
        break;
      case 'invert':
        applyInvert(resultData);
        break;
      case 'blur':
        applyBlur(resultData, width, height);
        break;
    }
  });
  
  return result;
}

function applyGrayscale(data: Uint8ClampedArray) {
  for (let i = 0; i < data.length; i += 4) {
    const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
    data[i] = data[i + 1] = data[i + 2] = avg;
  }
}

// Other image processing functions...

Performance Considerations for Worker Communication

Measuring Worker communication overhead:

// Performance test Worker
self.onmessage = async (e: MessageEvent<{ type: string; size: number }>) => {
  if (e.data.type === 'benchmark') {
    const { size } = e.data;
    
    // Test small messages
    const smallStart = performance.now();
    for (let i = 0; i < 1000; i++) {
      self.postMessage({ type: 'small', data: i });
    }
    const smallEnd = performance.now();
    
    // Test large messages
    const largeData = new ArrayBuffer(size);
    const largeStart = performance.now();
    for (let i = 0; i < 100; i++) {
      self.postMessage({ type: 'large', data: largeData }, [largeData]);
    }
    const largeEnd = performance.now();
    
    self.postMessage({
      type: 'benchmark-result',
      smallMessageTime: (smallEnd - smallStart) / 1000,
      largeMessageTime: (largeEnd - largeStart) / 100,
      transferrableRatio: size / (largeEnd - largeStart)
    });
  }
};

Parallel Computing Patterns in Workers

Implementing parallel task decomposition:

// parallel-worker.ts
interface Task {
  id: string;
  start: number;
  end: number;
  data: Float64Array;
}

interface Result {
  id: string;
  value: number;
}

self.onmessage = (e: MessageEvent<Task>) => {
  const { id, start, end, data } = e.data;
  let sum = 0;
  
  // Compute assigned portion
  for (let i = start; i < end; i++) {
    sum += data[i];
  }
  
  const result: Result = {
    id,
    value: sum
  };
  
  self.postMessage(result);
};

// Usage in main thread
async function parallelSum(data: Float64Array, chunkCount: number): Promise<number> {
  const chunkSize = Math.ceil(data.length / chunkCount);
  const workers: Worker[] = [];
  const promises: Promise<number>[] = [];
  
  for (let i = 0; i < chunkCount; i++) {
    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, data.length);
    const worker = new Worker(new URL('./parallel-worker.ts', import.meta.url));
    
    const promise = new Promise<number>((resolve) => {
      worker.onmessage = (e: MessageEvent<Result>) => {
        resolve(e.data.value);
        worker.terminate();
      };
    });
    
    workers.push(worker);
    promises.push(promise);
    
    // Send data copy to Worker
    const chunk = new Float64Array(data.buffer.slice(
      start * Float64Array.BYTES_PER_ELEMENT,
      end * Float64Array.BYTES_PER_ELEMENT
    ));
    
    worker.postMessage({
      id: `chunk-${i}`,
      start: 0,
      end: chunk.length,
      data: chunk
    }, [chunk.buffer]);
  }
  
  const results = await Promise.all(promises);
  return results.reduce((a, b) => a + b, 0);
}

Caching Strategies in Workers

Implementing data caching in Workers:

// caching-worker.ts
interface CacheItem {
  timestamp: number;
  data: unknown;
  ttl: number;
}

const cache = new Map<string, CacheItem>();
const MAX_CACHE_SIZE = 100;

function getFromCache(key: string): unknown | null {
  const item = cache.get(key);
  if (!item) return null;
  
  // Check if expired
  if (Date.now() - item.timestamp > item.ttl) {
    cache.delete(key);
    return null;
  }
  
  return item.data;
}

function setToCache(key: string, data: unknown, ttl: number = 60000) {
  if (cache.size >= MAX_CACHE_SIZE) {
    // Simple LRU strategy
    const oldestKey = cache.keys().next().value;
    cache.delete(oldestKey);
  }
  
  cache.set(key, {
    timestamp: Date.now(),
    data,
    ttl
  });
}

self.onmessage = async (e: MessageEvent<{ 
  type: 'get' | 'set'; 
  key: string; 
  data?: unknown; 
  ttl?: number 
}>) => {
  const { type, key, data, ttl } = e.data;
  
  if (type === 'get') {
    const cached = getFromCache(key);
    self.postMessage({
      type: 'cache-response',
      key,
      data: cached,
      hit: cached !== null
    });
  } else if (type === 'set' && data !== undefined) {
    setToCache(key, data, ttl);
    self.postMessage({
      type: 'cache-set',
      key,
      success: true
    });
  }
};

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

上一篇:与GraphQL配合

下一篇:与WebAssembly

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.