PatternsLists & Performance
Web Worker Data Sorting
Offload heavy data processing to a separate thread using Web Workers to keep the UI responsive.
Web Worker Data Sorting
Problem
Sorting large datasets (10,000+ items) on the Main Thread blocks the UI, causing freezes and poor user experience.
Solution
Use Web Workers to run sorting operations in a separate thread. This keeps the Main Thread free to handle user interactions and rendering.
// worker.ts - The Web Worker code
interface SortMessage {
type: 'sort';
data: unknown[];
sortKey?: string;
direction: 'asc' | 'desc';
}
interface SortResult {
type: 'sorted';
data: unknown[];
duration: number;
}
interface ErrorMessage {
type: 'error';
message: string;
}
type WorkerMessage = SortMessage;
type WorkerResponse = SortResult | ErrorMessage;
// Generic comparison function
function compareValues(
a: unknown,
b: unknown,
direction: 'asc' | 'desc'
): number {
const multiplier = direction === 'asc' ? 1 : -1;
if (a === b) return 0;
if (a === null || a === undefined) return 1 * multiplier;
if (b === null || b === undefined) return -1 * multiplier;
if (typeof a === 'string' && typeof b === 'string') {
return a.localeCompare(b) * multiplier;
}
if (typeof a === 'number' && typeof b === 'number') {
return (a - b) * multiplier;
}
if (a instanceof Date && b instanceof Date) {
return (a.getTime() - b.getTime()) * multiplier;
}
return String(a).localeCompare(String(b)) * multiplier;
}
// Get nested property value
function getNestedValue(obj: unknown, path: string): unknown {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
const keys = path.split('.');
let current: unknown = obj;
for (const key of keys) {
if (typeof current === 'object' && current !== null && key in current) {
current = (current as Record<string, unknown>)[key];
} else {
return undefined;
}
}
return current;
}
// Worker message handler
self.addEventListener('message', (event: MessageEvent<WorkerMessage>) => {
const { type, data, sortKey, direction } = event.data;
if (type !== 'sort') {
const errorResponse: ErrorMessage = {
type: 'error',
message: 'Unknown message type',
};
self.postMessage(errorResponse);
return;
}
try {
const startTime = performance.now();
const sorted = [...data].sort((a, b) => {
const valueA = sortKey ? getNestedValue(a, sortKey) : a;
const valueB = sortKey ? getNestedValue(b, sortKey) : b;
return compareValues(valueA, valueB, direction);
});
const duration = performance.now() - startTime;
const response: SortResult = {
type: 'sorted',
data: sorted,
duration,
};
self.postMessage(response);
} catch (error) {
const errorResponse: ErrorMessage = {
type: 'error',
message: error instanceof Error ? error.message : 'Unknown error',
};
self.postMessage(errorResponse);
}
});
// Main thread - Worker manager
interface SortOptions {
sortKey?: string;
direction: 'asc' | 'desc';
}
class WorkerSortManager {
private worker: Worker | null = null;
private readonly workerUrl: string;
constructor(workerScript: string) {
// Create worker from script string
const blob = new Blob([workerScript], { type: 'application/javascript' });
this.workerUrl = URL.createObjectURL(blob);
}
/**
* Initialize the worker
*/
private initWorker(): Worker {
if (!this.worker) {
this.worker = new Worker(this.workerUrl);
}
return this.worker;
}
/**
* Sort data using Web Worker
*/
public async sort<T>(data: T[], options: SortOptions): Promise<T[]> {
return new Promise((resolve, reject) => {
const worker = this.initWorker();
const handleMessage = (event: MessageEvent<WorkerResponse>): void => {
const response = event.data;
if (response.type === 'sorted') {
console.log(`Sorted ${data.length} items in ${response.duration.toFixed(2)}ms`);
worker.removeEventListener('message', handleMessage);
worker.removeEventListener('error', handleError);
resolve(response.data as T[]);
} else if (response.type === 'error') {
worker.removeEventListener('message', handleMessage);
worker.removeEventListener('error', handleError);
reject(new Error(response.message));
}
};
const handleError = (error: ErrorEvent): void => {
worker.removeEventListener('message', handleMessage);
worker.removeEventListener('error', handleError);
reject(new Error(`Worker error: ${error.message}`));
};
worker.addEventListener('message', handleMessage);
worker.addEventListener('error', handleError);
const message: SortMessage = {
type: 'sort',
data,
sortKey: options.sortKey,
direction: options.direction,
};
worker.postMessage(message);
});
}
/**
* Terminate the worker and clean up
*/
public terminate(): void {
if (this.worker) {
this.worker.terminate();
this.worker = null;
}
URL.revokeObjectURL(this.workerUrl);
}
}
// Practical example
interface Product {
id: string;
name: string;
price: number;
rating: number;
createdAt: Date;
}
// The worker script as a string (in production, use a separate file)
const workerScript = `
${compareValues.toString()}
${getNestedValue.toString()}
self.addEventListener('message', (event) => {
const { type, data, sortKey, direction } = event.data;
if (type !== 'sort') {
self.postMessage({ type: 'error', message: 'Unknown message type' });
return;
}
try {
const startTime = performance.now();
const sorted = [...data].sort((a, b) => {
const valueA = sortKey ? getNestedValue(a, sortKey) : a;
const valueB = sortKey ? getNestedValue(b, sortKey) : b;
return compareValues(valueA, valueB, direction);
});
const duration = performance.now() - startTime;
self.postMessage({
type: 'sorted',
data: sorted,
duration,
});
} catch (error) {
self.postMessage({
type: 'error',
message: error.message,
});
}
});
`;
// Usage
async function sortProductList(
products: Product[],
container: HTMLElement
): Promise<void> {
const sortManager = new WorkerSortManager(workerScript);
try {
// UI remains responsive during this operation
const sorted = await sortManager.sort(products, {
sortKey: 'price',
direction: 'desc',
});
// Render sorted results
container.innerHTML = '';
sorted.forEach((product) => {
const item = document.createElement('div');
item.innerHTML = `
<h3>${product.name}</h3>
<p>Price: $${product.price}</p>
<p>Rating: ${product.rating}/5</p>
`;
container.appendChild(item);
});
} catch (error) {
console.error('Sort failed:', error);
container.innerHTML = '<p>Failed to sort products</p>';
} finally {
sortManager.terminate();
}
}Performance Note
Web Workers run on a separate CPU thread, completely independent from the Main Thread. This means:
- UI stays responsive: users can scroll, click, and interact during heavy processing
- No frame drops: 60fps maintained even during sorting of 100,000+ items
- Better UX: no "freezing" or "not responding" states
Benchmark (10,000 complex objects):
- Main Thread: 300ms (UI frozen)
- Web Worker: 300ms processing + 0ms UI blocking
Important considerations:
- Data transfer between threads uses structured cloning (adds 5-10ms overhead)
- Workers can't access DOM directly
- Best for operations taking longer than 50ms
- Worker creation has ~10ms overhead (reuse workers when possible)
Use Web Workers for:
- Sorting/filtering large datasets
- Complex calculations (statistics, algorithms)
- Data transformations (parsing, formatting)
- Image processing