
Efficient Event and Task Scheduling with a Min-Binary Heap in JavaScript
When orchestrating concurrent asynchronous tasks, scheduling browser animations, or routing pathfinding nodes, client-side applications frequently need to evaluate items in order of urgent priority. A standard shortcut in JavaScript is pushing elements into a regular array and calling array.sort() whenever the highest-priority item is required. Because quicksort and Timsort execute in $O(n \log n)$ time, re-sorting on every job arrival or cycle triggers severe computational penalties and stalls task dispatch loops on high-throughput workloads.
A binary min-heap offers the ideal algorithmic foundation for priority queues. Represented compactly within a contiguous array, the root node occupies index 0, with children of index $i$ mapped mathematically to indices $2i + 1$ and $2i + 2$, and parents located at $\lfloor (i - 1) / 2 \rfloor$. By maintaining the invariant where every parent evaluates smaller than or equal to its children, the heap achieves instant $O(1)$ inspection of the minimum item alongside $O(\log n)$ insertion and extraction via efficient bubble-up and sink-down element swaps.
Example
import { PriorityQueue } from './PriorityQueue.js';
// Instantiate min-heap prioritized by scheduled timestamp
const taskScheduler = new PriorityQueue((a, b) => a.timestamp - b.timestamp);
// Enqueue asynchronous jobs with varied due times
taskScheduler.enqueue({ name: 'Sync telemetry', timestamp: 1718899200000 });
taskScheduler.enqueue({ name: 'Urgent user payment', timestamp: 1718895600000 });
taskScheduler.enqueue({ name: 'Background indexing', timestamp: 1718902800000 });
// Peek at the most urgent pending task without removing it
console.log(taskScheduler.peek().name); // 'Urgent user payment'
// Dequeue and process tasks in strict priority order
while (!taskScheduler.isEmpty()) {
const currentJob = taskScheduler.dequeue();
console.log(`Processing ${currentJob.name} (${currentJob.timestamp})`);
}
Implementation
/**
* Zero-dependency generic Priority Queue backed by a binary heap.
* Supports custom comparator functions for min-heap or max-heap behaviors.
*/
export class PriorityQueue {
/**
* @param {Function} [comparator] - Function returning negative if a < b, positive if a > b.
* Defaults to a standard ascending number comparator.
*/
constructor(comparator = (a, b) => a - b) {
this._heap = [];
this._comparator = comparator;
}
/**
* Returns current count of queued items.
* @returns {number}
*/
get size() {
return this._heap.length;
}
/**
* Checks if queue has zero items.
* @returns {boolean}
*/
isEmpty() {
return this._heap.length === 0;
}
/**
* Inspects the highest-priority element in O(1) time.
* @returns {*|undefined}
*/
peek() {
return this._heap[0];
}
/**
* Inserts an item into the queue in O(log n) time.
* Appends to end of array and bubbles up until heap property is restored.
* @param {*} value
*/
enqueue(value) {
this._heap.push(value);
this._bubbleUp(this._heap.length - 1);
}
/**
* Extracts and returns the highest-priority element in O(log n) time.
* Swaps root with final leaf, pops leaf, and sinks new root down.
* @returns {*|undefined}
*/
dequeue() {
if (this.isEmpty()) return undefined;
const root = this._heap[0];
const bottom = this._heap.pop();
if (this._heap.length > 0) {
this._heap[0] = bottom;
this._sinkDown(0);
}
return root;
}
/**
* Swaps two array elements in-place.
* @private
*/
_swap(i, j) {
const temp = this._heap[i];
this._heap[i] = this._heap[j];
this._heap[j] = temp;
}
/**
* Restores heap property upward from newly inserted index.
* @private
*/
_bubbleUp(index) {
let current = index;
while (current > 0) {
const parent = Math.floor((current - 1) / 2);
// If current is higher priority than parent, swap
if (this._comparator(this._heap[current], this._heap[parent]) < 0) {
this._swap(current, parent);
current = parent;
} else {
break;
}
}
}
/**
* Restores heap property downward from index after root removal.
* @private
*/
_sinkDown(index) {
const length = this._heap.length;
let current = index;
while (true) {
const leftChild = 2 * current + 1;
const rightChild = 2 * current + 2;
let highestPriority = current;
if (
leftChild < length &&
this._comparator(this._heap[leftChild], this._heap[highestPriority]) < 0
) {
highestPriority = leftChild;
}
if (
rightChild < length &&
this._comparator(this._heap[rightChild], this._heap[highestPriority]) < 0
) {
highestPriority = rightChild;
}
if (highestPriority !== current) {
this._swap(current, highestPriority);
current = highestPriority;
} else {
break;
}
}
}
}
// Concrete application: Async task scheduler with priority leveling
class AsyncTaskScheduler {
constructor() {
// Items with lowest priority numbers are executed first (1 = critical, 5 = low)
this.queue = new PriorityQueue((a, b) => a.priority - b.priority);
this.isProcessing = false;
}
scheduleTask(id, priority, taskFn) {
this.queue.enqueue({ id, priority, taskFn, addedAt: Date.now() });
this.processNext();
}
async processNext() {
if (this.isProcessing || this.queue.isEmpty()) return;
this.isProcessing = true;
const current = this.queue.dequeue();
try {
console.log(`Executing Task [${current.id}] (Priority ${current.priority})`);
await current.taskFn();
} catch (err) {
console.error(`Task [${current.id}] failed:`, err);
} finally {
this.isProcessing = false;
this.processNext();
}
}
}
// Verification simulation
const scheduler = new AsyncTaskScheduler();
scheduler.scheduleTask('log-analytics', 3, () => new Promise(r => setTimeout(r, 50)));
scheduler.scheduleTask('auth-token-refresh', 1, () => new Promise(r => setTimeout(r, 50)));
scheduler.scheduleTask('render-frame', 2, () => new Promise(r => setTimeout(r, 50)));
Main Point: Implementing a binary min-heap Priority Queue in JavaScript reduces insertion and extraction costs to logarithmic time while delivering predictable O(1) peek performance for mission-critical scheduling engines.
Credits
Photo by Pawel Czerwinski on Unsplash