Skip to content

Performance Optimization ​

Home > Documentation > Performance > Optimization

Overview ​

ParleyJS is designed for high-performance inter-context communication. This guide covers optimization techniques to maximize throughput, minimize latency, and reduce memory usage in your applications.

Table of Contents ​

  1. Message Batching
  2. Payload Size Optimization
  3. Connection Pooling
  4. Memory Management
  5. Debouncing and Throttling
  6. Transferable Objects
  7. Performance Profiling
  8. Benchmarking
  9. Optimization Checklist

Message Batching ​

Batching multiple operations into single messages reduces overhead and improves throughput.

Problem: High Message Frequency ​

typescript
// SLOW - 1000 individual messages
for (let i = 0; i < 1000; i++) {
    await parley.send('process-item', { id: i, data: items[i] });
}

Solution: Batch Processing ​

typescript
// FAST - Single batched message
await parley.send('process-batch', {
    items: items.map((data, id) => ({ id, data })),
});

Implementation ​

typescript
// Sender: Accumulate items and send in batches
class BatchProcessor {
    private batch: any[] = [];
    private batchSize = 100;
    private timeout: NodeJS.Timeout | null = null;

    async addItem(item: any) {
        this.batch.push(item);

        if (this.batch.length >= this.batchSize) {
            await this.flush();
        } else if (!this.timeout) {
            // Auto-flush after 100ms of inactivity
            this.timeout = setTimeout(() => this.flush(), 100);
        }
    }

    private async flush() {
        if (this.batch.length === 0) return;

        const items = this.batch.splice(0, this.batch.length);

        if (this.timeout) {
            clearTimeout(this.timeout);
            this.timeout = null;
        }

        await parley.send('process-batch', { items });
    }
}

// Usage
const processor = new BatchProcessor();

for (const item of items) {
    await processor.addItem(item);
}

Receiver: Process Batches Efficiently ​

typescript
parley.on('process-batch', async (payload, respond) => {
    const results = await Promise.all(
        payload.items.map((item) => processItem(item))
    );

    respond({ results, count: results.length });
});

Performance Gain: 10-50x throughput improvement for high-frequency operations.


Payload Size Optimization ​

Minimize payload size to reduce serialization overhead and network transfer time.

Technique 1: Send Only What's Needed ​

typescript
// SLOW - Sending entire user object (5KB)
await parley.send('update-name', {
    user: {
        id: 123,
        username: 'alice',
        email: 'alice@example.com',
        profile: {
            /* large nested object */
        },
        settings: {
            /* large nested object */
        },
    },
    newName: 'Alice Smith',
});

// FAST - Send only required fields (200 bytes)
await parley.send('update-name', {
    userId: 123,
    newName: 'Alice Smith',
});

Technique 2: Compress Large Payloads ​

typescript
import pako from 'pako';

// Compress before sending
async function sendCompressed(channel: string, data: any) {
    const json = JSON.stringify(data);
    const compressed = pako.deflate(json);

    await parley.send(channel, {
        compressed: true,
        data: Array.from(compressed), // Convert to regular array
    });
}

// Decompress on receive
parley.on('data-channel', (payload, respond) => {
    if (payload.compressed) {
        const decompressed = pako.inflate(new Uint8Array(payload.data), {
            to: 'string',
        });
        const data = JSON.parse(decompressed);
        processData(data);
    } else {
        processData(payload.data);
    }

    respond({ received: true });
});

Technique 3: Use Efficient Data Structures ​

typescript
// SLOW - Array of objects (verbose JSON)
const data = [
    { id: 1, name: 'Alice', score: 95 },
    { id: 2, name: 'Bob', score: 87 },
    { id: 3, name: 'Carol', score: 92 },
];

// FAST - Columnar format (50% smaller)
const optimized = {
    ids: [1, 2, 3],
    names: ['Alice', 'Bob', 'Carol'],
    scores: [95, 87, 92],
};

Performance Gain: 2-5x reduction in serialization time for large payloads.


Connection Pooling ​

Reuse Parley instances and connections instead of creating new ones.

Anti-Pattern: Creating New Instances ​

typescript
// SLOW - Creates new connection every time
async function sendMessage(data: any) {
    const parley = new Parley({ target: worker });
    await parley.send('process', data);
    parley.destroy(); // Tears down connection
}

Best Practice: Reuse Instances ​

typescript
// FAST - Single reusable connection
const parley = new Parley({ target: worker });

async function sendMessage(data: any) {
    return parley.send('process', data);
}

// Cleanup on app shutdown
window.addEventListener('beforeunload', () => {
    parley.destroy();
});

Worker Pool Pattern ​

typescript
class WorkerPool {
    private workers: Parley[] = [];
    private currentIndex = 0;

    constructor(size: number = 4) {
        for (let i = 0; i < size; i++) {
            const worker = new Worker('/worker.js');
            this.workers.push(new Parley({ target: worker }));
        }
    }

    async send(channel: string, payload: any) {
        // Round-robin distribution
        const parley = this.workers[this.currentIndex];
        this.currentIndex = (this.currentIndex + 1) % this.workers.length;

        return parley.send(channel, payload);
    }

    destroy() {
        this.workers.forEach((p) => p.destroy());
    }
}

// Usage
const pool = new WorkerPool(4); // 4 workers

await Promise.all([
    pool.send('task', data1), // Worker 0
    pool.send('task', data2), // Worker 1
    pool.send('task', data3), // Worker 2
    pool.send('task', data4), // Worker 3
    pool.send('task', data5), // Worker 0 (round-robin)
]);

Performance Gain: Eliminates connection setup overhead (10-100ms per connection).


Memory Management ​

Prevent memory leaks and reduce garbage collection pressure.

Technique 1: Unsubscribe from Handlers ​

typescript
// BAD - Handler never cleaned up
parley.on('temporary-event', handler);

// GOOD - Unsubscribe when no longer needed
const unsubscribe = parley.on('temporary-event', handler);

// Later...
unsubscribe();

Technique 2: Clear References to Large Objects ​

typescript
parley.on('process-image', async (payload, respond) => {
    // Process large image data
    const processed = await processImage(payload.imageData);

    respond({ success: true });

    // Clear reference to allow garbage collection
    payload.imageData = null;
    processed = null;
});

Technique 3: Use WeakMap for Caching ​

typescript
// BAD - Prevents garbage collection
const cache = new Map<string, any>();

parley.on('get-data', (payload, respond) => {
    if (cache.has(payload.key)) {
        respond(cache.get(payload.key));
    } else {
        const data = fetchData(payload.key);
        cache.set(payload.key, data);
        respond(data);
    }
});

// GOOD - Allows garbage collection
const cache = new WeakMap<object, any>();

parley.on('get-data', (payload, respond) => {
    const keyObj = { key: payload.key };

    if (cache.has(keyObj)) {
        respond(cache.get(keyObj));
    } else {
        const data = fetchData(payload.key);
        cache.set(keyObj, data);
        respond(data);
    }
});

Technique 4: Implement Message Queue Limits ​

typescript
class RateLimitedParley {
    private queue: Array<() => Promise<void>> = [];
    private maxQueue = 100;
    private processing = 0;
    private maxConcurrent = 10;

    async send(channel: string, payload: any) {
        if (this.queue.length >= this.maxQueue) {
            throw new Error('Queue full - message dropped');
        }

        return new Promise((resolve, reject) => {
            this.queue.push(async () => {
                try {
                    this.processing++;
                    const result = await parley.send(channel, payload);
                    resolve(result);
                } catch (error) {
                    reject(error);
                } finally {
                    this.processing--;
                    this.processQueue();
                }
            });

            this.processQueue();
        });
    }

    private processQueue() {
        while (this.queue.length > 0 && this.processing < this.maxConcurrent) {
            const task = this.queue.shift();
            if (task) task();
        }
    }
}

Performance Gain: Prevents memory leaks and reduces GC pauses.


Debouncing and Throttling ​

Control message frequency for events that fire rapidly.

Debouncing: Send After Inactivity ​

typescript
class DebouncedSender {
    private timeout: NodeJS.Timeout | null = null;
    private delay = 300;

    send(channel: string, payload: any) {
        if (this.timeout) {
            clearTimeout(this.timeout);
        }

        this.timeout = setTimeout(() => {
            parley.emit(channel, payload);
            this.timeout = null;
        }, this.delay);
    }
}

// Usage: User typing in search box
const debounced = new DebouncedSender();

searchInput.addEventListener('input', (e) => {
    debounced.send('search', { query: e.target.value });
});

Throttling: Limit Send Frequency ​

typescript
class ThrottledSender {
    private lastSent = 0;
    private interval = 100; // Max 10 messages per second

    send(channel: string, payload: any) {
        const now = Date.now();

        if (now - this.lastSent >= this.interval) {
            parley.emit(channel, payload);
            this.lastSent = now;
        }
    }
}

// Usage: Mouse movement tracking
const throttled = new ThrottledSender();

document.addEventListener('mousemove', (e) => {
    throttled.send('mouse-position', { x: e.clientX, y: e.clientY });
});

Combined: Debounce + Throttle ​

typescript
class SmartSender {
    private timeout: NodeJS.Timeout | null = null;
    private lastSent = 0;
    private throttleMs = 100;
    private debounceMs = 300;

    send(channel: string, payload: any) {
        const now = Date.now();

        // Clear existing debounce
        if (this.timeout) {
            clearTimeout(this.timeout);
        }

        // Throttle: Send immediately if enough time has passed
        if (now - this.lastSent >= this.throttleMs) {
            parley.emit(channel, payload);
            this.lastSent = now;
        } else {
            // Debounce: Schedule send
            this.timeout = setTimeout(() => {
                parley.emit(channel, payload);
                this.lastSent = Date.now();
                this.timeout = null;
            }, this.debounceMs);
        }
    }
}

Performance Gain: 10-100x reduction in message frequency for high-rate events.


Transferable Objects ​

Transfer ownership of ArrayBuffers and other transferable objects for zero-copy performance.

Without Transfer (Copy) ​

typescript
// SLOW - Copies 10MB of data
const buffer = new ArrayBuffer(10 * 1024 * 1024);
await parley.send('process-buffer', { buffer });

// Original buffer is still accessible (data was copied)
console.log(buffer.byteLength); // 10485760

With Transfer (Zero-Copy) ​

typescript
// FAST - Transfers ownership (zero-copy)
const buffer = new ArrayBuffer(10 * 1024 * 1024);
await parley.send(
    'process-buffer',
    { buffer },
    {
        transfer: [buffer],
    }
);

// Original buffer is now detached
console.log(buffer.byteLength); // 0 (neutered)

Receiver Side ​

typescript
parley.on('process-buffer', (payload, respond) => {
    // Received buffer without copying
    const { buffer } = payload;

    const view = new Uint8Array(buffer);
    // Process buffer...

    // Transfer back if needed
    respond({ result: 'done', buffer }, { transfer: [buffer] });
});

Transferable Types ​

typescript
// All transferable types
const transferables = [
    new ArrayBuffer(1024),
    new MessagePort(),
    new ImageBitmap(),
    new OffscreenCanvas(100, 100),
];

await parley.send(
    'process',
    { data: transferables },
    {
        transfer: transferables,
    }
);

Performance Gain: 100-1000x faster for large binary data (no copying overhead).


Performance Profiling ​

Measure and identify bottlenecks in your ParleyJS usage.

Built-in Performance Monitoring ​

typescript
const parley = new Parley({ debug: true });

// Enable performance marks
parley.on('message:sent', (event) => {
    performance.mark(`parley-send-${event.messageId}`);
});

parley.on('message:received', (event) => {
    performance.mark(`parley-receive-${event.messageId}`);
    performance.measure(
        `parley-roundtrip-${event.messageId}`,
        `parley-send-${event.messageId}`,
        `parley-receive-${event.messageId}`
    );
});

// Analyze measurements
const measures = performance.getEntriesByType('measure');
const avgLatency =
    measures.reduce((sum, m) => sum + m.duration, 0) / measures.length;
console.log('Average round-trip latency:', avgLatency, 'ms');

Custom Performance Wrapper ​

typescript
class PerformanceParley {
    private parley: Parley;
    private stats = {
        messagesSent: 0,
        messagesReceived: 0,
        totalLatency: 0,
        errors: 0,
    };

    constructor(config: ParleyConfig) {
        this.parley = new Parley(config);
    }

    async send(channel: string, payload: any, options?: SendOptions) {
        const start = performance.now();

        try {
            this.stats.messagesSent++;
            const result = await this.parley.send(channel, payload, options);
            this.stats.messagesReceived++;
            this.stats.totalLatency += performance.now() - start;
            return result;
        } catch (error) {
            this.stats.errors++;
            throw error;
        }
    }

    getStats() {
        return {
            ...this.stats,
            avgLatency: this.stats.totalLatency / this.stats.messagesReceived,
            errorRate: this.stats.errors / this.stats.messagesSent,
        };
    }

    resetStats() {
        this.stats = {
            messagesSent: 0,
            messagesReceived: 0,
            totalLatency: 0,
            errors: 0,
        };
    }
}

// Usage
const parley = new PerformanceParley({ target: worker });

await parley.send('task', data);
console.log(parley.getStats());
// { messagesSent: 1, messagesReceived: 1, avgLatency: 15.2, errorRate: 0 }

Benchmarking ​

Compare different optimization strategies.

Benchmark Framework ​

typescript
async function benchmark(
    name: string,
    fn: () => Promise<void>,
    iterations = 1000
) {
    const start = performance.now();

    for (let i = 0; i < iterations; i++) {
        await fn();
    }

    const duration = performance.now() - start;
    const avgTime = duration / iterations;
    const opsPerSec = 1000 / avgTime;

    console.log(`${name}:
        Total: ${duration.toFixed(2)}ms
        Average: ${avgTime.toFixed(2)}ms
        Throughput: ${opsPerSec.toFixed(0)} ops/sec
    `);
}

// Compare batched vs individual sends
await benchmark(
    'Individual sends',
    async () => {
        for (let i = 0; i < 100; i++) {
            await parley.send('process', { id: i });
        }
    },
    10
);

await benchmark(
    'Batched sends',
    async () => {
        const items = Array.from({ length: 100 }, (_, i) => ({ id: i }));
        await parley.send('process-batch', { items });
    },
    10
);

Output Example ​

Individual sends:
    Total: 1523.45ms
    Average: 152.35ms
    Throughput: 7 ops/sec

Batched sends:
    Total: 85.23ms
    Average: 8.52ms
    Throughput: 117 ops/sec

Optimization Checklist ​

Use this checklist to optimize your ParleyJS implementation:

Message Optimization ​

  • [ ] Batch high-frequency operations (>10 messages/sec)
  • [ ] Send only required fields, not entire objects
  • [ ] Use columnar or compressed formats for large payloads
  • [ ] Implement debouncing for user input events
  • [ ] Implement throttling for high-rate events (mouse, scroll)

Connection Management ​

  • [ ] Reuse Parley instances instead of creating new ones
  • [ ] Use worker pools for parallel processing
  • [ ] Close/destroy connections when no longer needed
  • [ ] Implement connection retry logic with exponential backoff

Memory Management ​

  • [ ] Unsubscribe from event handlers when done
  • [ ] Clear references to large objects after processing
  • [ ] Implement message queue limits to prevent overflow
  • [ ] Use WeakMap for caches that should allow GC
  • [ ] Monitor memory usage with Chrome DevTools

Data Transfer ​

  • [ ] Use transferable objects for ArrayBuffers >1MB
  • [ ] Transfer ImageBitmap instead of canvas data
  • [ ] Avoid transferring the same buffer multiple times

Performance Monitoring ​

  • [ ] Enable debug mode during development
  • [ ] Use Performance API to measure latency
  • [ ] Track error rates and timeout frequency
  • [ ] Benchmark critical message paths
  • [ ] Set up monitoring for production

Configuration ​

  • [ ] Set appropriate timeout values per operation
  • [ ] Enable heartbeat monitoring for long-lived connections
  • [ ] Configure allowed origins for security
  • [ ] Use schemas for validation to catch errors early

Code Quality ​

  • [ ] Use TypeScript for type safety
  • [ ] Implement error handling for all sends
  • [ ] Add retry logic for transient failures
  • [ ] Use async/await instead of promise chains
  • [ ] Follow single responsibility principle in handlers

Previous: Performance README

Related:

Back to: Performance

Released under the MIT License.