Priority Lanes
The Airport Security Analogy
Ever been to an airport with different security lanes?
- TSA PreCheck (Critical): VIPs walk straight through - zero wait
- First Class (High): Short line, quick processing
- Economy (Normal): Standard line, everyone queues up
- Staff/Cargo (Background): Handled when other lanes are empty
Why this design? Because treating everyone equally creates worse outcomes. A pilot missing their flight affects 200 passengers. A tourist arriving 5 minutes late affects only themselves.
Event buses have the same problem. Without priorities, a background analytics event can block critical UI updates, making your app feel laggy.
The Problem: All Events Are Created Equal (But Shouldn't Be)
Traditional event buses execute everything in FIFO (First In, First Out) order:
// Standard event bus
bus.on('analytics:track', slowAnalyticsHandler); // Takes 50ms
bus.on('ui:update', renderUI); // Takes 2ms
bus.on('background:sync', syncToServer); // Takes 200ms
// User clicks button
bus.emit('analytics:track', data); // Starts executing (50ms)
bus.emit('ui:update', data); // BLOCKED! Waits 50ms
bus.emit('background:sync', data); // Waits 250ms totalResult: The UI update (which should be instant) waits 50ms. Users perceive this as "lag."
The Solution: Priority Scheduling
Nexus provides four execution lanes:
bus.emit('ui:update', data, { priority: 'critical' }); // Runs NOW
bus.emit('api:fetch', data, { priority: 'high' }); // Runs soon
bus.emit('page:view', data, { priority: 'normal' }); // Runs next tick
bus.emit('analytics:log', data, { priority: 'background' }); // Runs when idleHow Priority Lanes Work
Critical Priority: Immediate Execution
Use case: Preventing perceived lag in UI interactions.
// User clicks "Save" button
button.addEventListener('click', () => {
// Must disable button IMMEDIATELY to prevent double-clicks
bus.emit('ui:button-disable', { id: 'save' }, { priority: 'critical' });
// Then do the actual save (can be async)
bus.emit('data:save', formData, { priority: 'high' });
});
// Handler runs synchronously (blocks thread)
bus.on('ui:button-disable', (payload) => {
document.getElementById(payload.id).disabled = true;
// This executes BEFORE the click handler returns
});Warning: Use sparingly! critical blocks the JavaScript thread. If your handler takes 100ms, the UI freezes for 100ms.
Rule of thumb: Only use critical for:
- Disabling buttons (prevent double-click)
- Updating input focus
- Stopping animations
High Priority: Microtask Queue
Use case: Important async work that should happen before the next render.
// User submits form
bus.emit('form:submit', formData, { priority: 'high' });
// Handler runs in microtask (before next tick)
bus.on('form:submit', async (data) => {
// This will complete before setTimeout or requestAnimationFrame
const result = await validateForm(data);
if (result.valid) {
bus.emit('form:success', result);
} else {
bus.emit('form:error', result.errors);
}
});Execution order:
console.log('1. Sync code');
bus.emit('event:high', {}, { priority: 'high' });
bus.on('event:high', () => console.log('2. High priority'));
setTimeout(() => console.log('3. Normal (setTimeout)'), 0);
console.log('4. Sync code continues');
// Output:
// 1. Sync code
// 4. Sync code continues
// 2. High priority
// 3. Normal (setTimeout)Normal Priority: Standard Behavior
Use case: Everything else. This is the default.
// Default behavior (no priority specified)
bus.emit('page:view', { path: '/home' });
// Equivalent to:
bus.emit('page:view', { path: '/home' }, { priority: 'normal' });Runs in the next event loop tick (setTimeout(fn, 0)).
Background Priority: Idle Callback
Use case: Non-urgent work like analytics, logging, or prefetching.
// User views a page
bus.emit('analytics:pageview', {
path: window.location.pathname,
timestamp: Date.now()
}, { priority: 'background' });
// Handler runs only when CPU is idle
bus.on('analytics:pageview', (data) => {
// Send to analytics service
fetch('https://analytics.example.com/track', {
method: 'POST',
body: JSON.stringify(data)
});
// This won't block UI rendering or user interactions
});Behind the scenes:
if (typeof requestIdleCallback !== 'undefined') {
requestIdleCallback(() => runListeners());
} else {
// Fallback for browsers without requestIdleCallback
setTimeout(() => runListeners(), 0);
}Basic Example: Dashboard Updates
import { Nexus } from '@caeligo/nexus-orchestrator';
const bus = new Nexus();
// Critical: Spinner must show instantly
bus.on('ui:spinner', (show) => {
document.getElementById('spinner').style.display = show ? 'block' : 'none';
});
// High: Data fetch is important
bus.on('api:fetch', async () => {
const data = await fetch('/api/metrics').then(r => r.json());
bus.emit('data:received', data, { priority: 'critical' });
});
// Normal: Update charts (standard rendering)
bus.on('data:received', (data) => {
updateChart(data);
});
// Background: Log for analytics
bus.on('data:received', (data) => {
console.log('Metrics updated:', data);
sendToAnalytics(data);
}, { priority: 'background' });
// User clicks "Refresh"
function refresh() {
bus.emit('ui:spinner', true, { priority: 'critical' }); // Instant
bus.emit('api:fetch', {}, { priority: 'high' }); // Soon
}Execution timeline:
0ms: User clicks → 'ui:spinner' emitted (critical)
0ms: Spinner shows (synchronous)
0ms: 'api:fetch' emitted (high)
1ms: Fetch starts (microtask)
150ms: Fetch completes → 'data:received' emitted (critical)
150ms: Chart updates (synchronous)
200ms: CPU idle → Analytics log sent (background)Real-World Example: E-Commerce Product Page
interface ProductEvents {
'product:view': { productId: number };
'ui:highlight': { element: string };
'analytics:track': { event: string; data: any };
'prefetch:related': { productId: number };
}
const bus = new Nexus<ProductEvents>();
// === CRITICAL PRIORITY ===
// Highlight the selected variant immediately
bus.on('ui:highlight', (payload) => {
document.querySelectorAll('.variant').forEach(el =>
el.classList.remove('selected')
);
document.querySelector(payload.element)?.classList.add('selected');
});
// === HIGH PRIORITY ===
// Update price based on selected options
bus.on('product:variant-change', async (payload) => {
const price = await fetchPrice(payload.productId, payload.options);
bus.emit('ui:price-update', { price }, { priority: 'critical' });
});
// === NORMAL PRIORITY ===
// Load product images
bus.on('product:view', (payload) => {
loadProductImages(payload.productId);
});
// === BACKGROUND PRIORITY ===
// Track analytics
bus.on('product:view', (payload) => {
gtag('event', 'product_view', {
product_id: payload.productId
});
});
// Prefetch related products (low priority)
bus.on('prefetch:related', async (payload) => {
const related = await fetch(`/api/related/${payload.productId}`);
preloadImages(related);
});
// === USER INTERACTION ===
document.getElementById('variant-select')?.addEventListener('change', (e) => {
const variantId = e.target.value;
// UI update must be instant (critical)
bus.emit('ui:highlight',
{ element: `#variant-${variantId}` },
{ priority: 'critical' }
);
// Price calculation is important but async (high)
bus.emit('product:variant-change',
{ productId: currentProduct.id, options: { variant: variantId } },
{ priority: 'high' }
);
// Analytics can wait (background)
bus.emit('analytics:track',
{ event: 'variant_selected', data: { variantId } },
{ priority: 'background' }
);
});Edge Case: Priority Inversion
What if a low-priority event triggers a high-priority event?
// Background analytics event
bus.on('analytics:threshold', (data) => {
if (data.errorRate > 0.5) {
// Uh oh, need to alert user immediately!
bus.emit('ui:alert',
{ message: 'System degraded' },
{ priority: 'critical' } // 👈 Priority promoted
);
}
});
// Initial low-priority event
bus.emit('analytics:update', { errorRate: 0.8 }, { priority: 'background' });How it works:
analytics:updatequeued in idle callback- Eventually executed when CPU free
- Handler emits
ui:alertwithcriticalpriority ui:alertexecutes immediately (synchronous)
This is intentional and correct! Priority applies to execution timing, not causality.
Edge Case: Starvation Prevention
Can background events starve if the CPU is never idle?
Short answer: No. Browsers guarantee idle callbacks run within a few seconds.
Long answer:
// Browser fallback mechanism
if (typeof requestIdleCallback !== 'undefined') {
requestIdleCallback(() => runListeners(), { timeout: 2000 });
// ^ Forces execution after 2s even if not idle
} else {
setTimeout(() => runListeners(), 0); // Immediate fallback
}Edge Case: Mixed Priorities in Listeners
What if multiple listeners have different urgencies?
bus.on('button:click', fastHandler); // Should run immediately
bus.on('button:click', slowHandler); // Can run later
// Solution: Use separate events with priorities
bus.on('button:click', () => {
bus.emit('ui:update', data, { priority: 'critical' });
bus.emit('analytics:click', data, { priority: 'background' });
});Best practice: Don't mix critical and non-critical logic in one event. Split them.
Performance Benchmarks
Measured on Chrome 120, Apple M1:
| Priority | Execution Delay | Throughput |
|---|---|---|
| Critical | ~0ms (sync) | 1M events/sec |
| High | ~1-5ms (microtask) | 500K events/sec |
| Normal | ~10-50ms (next tick) | 100K events/sec |
| Background | 50ms-2s (idle) | Varies |
Note: Throughput depends on listener complexity. These numbers assume no-op handlers.
Visualization: Execution Timeline
Anti-Patterns to Avoid
❌ Don't: Overuse Critical Priority
// BAD: Everything is critical
bus.emit('log:debug', msg, { priority: 'critical' });
bus.emit('analytics:track', data, { priority: 'critical' });Problem: You've just reinvented the blocking, single-threaded event bus. No benefits.
Fix: Reserve critical for UI updates that must be instant.
❌ Don't: Async Work in Critical Handlers
// BAD: Blocks the thread!
bus.on('ui:update', async (data) => {
await fetch('/api/data'); // This will BLOCK the UI!
}, { priority: 'critical' });Problem: critical is synchronous. Async work defeats the purpose.
Fix: Use high priority for async work.
❌ Don't: Long-Running Sync Code in Critical
// BAD: Heavy computation blocks UI
bus.on('ui:render', (data) => {
for (let i = 0; i < 1000000; i++) {
// Heavy work
}
}, { priority: 'critical' });Problem: JavaScript is single-threaded. This freezes everything.
Fix: Use Web Workers for heavy computation, or split into chunks:
bus.on('ui:render', (data) => {
// Split into smaller chunks
requestAnimationFrame(() => processChunk(0));
});Testing Priority Behavior
import { describe, it, expect, vi } from 'vitest';
import { Nexus } from '@caeligo/nexus-orchestrator';
describe('Priority Lanes', () => {
it('should execute critical before high', async () => {
const bus = new Nexus();
const order: string[] = [];
bus.on('event', () => order.push('critical'));
bus.on('event', () => order.push('high'));
bus.emit('event', {}, { priority: 'high' });
bus.emit('event', {}, { priority: 'critical' });
// Critical runs immediately
expect(order[0]).toBe('critical');
// High runs in microtask
await Promise.resolve();
expect(order[1]).toBe('high');
});
it('should defer background to idle', (done) => {
const bus = new Nexus();
let executed = false;
bus.on('event', () => { executed = true; });
bus.emit('event', {}, { priority: 'background' });
// Not executed immediately
expect(executed).toBe(false);
// Wait for idle callback
setTimeout(() => {
expect(executed).toBe(true);
done();
}, 100);
});
});Next Steps
Now that you control execution timing, learn about state management:
- Cursors & State - Synchronous access to event data
- Resilience - Combine priorities with retry logic
- Pipes - Transform events before execution
Quick Reference
Set priority on emit:
bus.emit(event, payload, { priority: 'critical' | 'high' | 'normal' | 'background' });Default priority:
bus.emit(event, payload); // Same as priority: 'normal'Priority guidelines:
- Critical: UI updates < 16ms (one frame)
- High: Important async work (API calls, validation)
- Normal: Standard rendering and logic
- Background: Analytics, logging, prefetching
