Library / SDK
luciopaiva/heapify avatar
luciopaiva/heapify

heapify packs a priority queue into two parallel typed arrays

The fastest JavaScript priority queue out there. Zero dependencies.

743 stars22 forksTypeScriptMIT

At a glance

What is it?
heapify is a zero-dependency JavaScript priority queue built on a binary heap stored in two parallel typed arrays. It is small, fast in the project's own benchmark, and deliberately limited: fixed capacity, unsigned 32-bit defaults, and no stability guarantee for equal priorities.
Who is it for?
heapify fits job scheduling, pathfinding and event ordering code that lives in Node.js or the browser and needs a priority queue without pulling in a dependency. It is a poor fit if you need a stable ordering for equal priorities, keys or priorities outside the unsigned integer range, or a queue that grows past the capacity you passed to the constructor.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 133 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Two parallel typed arrays hold the whole heap

heapify implements a priority queue as a binary heap, and the heap itself is implemented as two parallel typed arrays: one for keys, one for priorities. The keys and priorities of a given entry live at the same index in both arrays, so the structure can return a key from pop() and the matching priority from peekPriority() without searching or pointer chasing. The constructor defaults both backing arrays to Uint32Array, which means every key and priority is an unsigned 32-bit integer by default.

The library is written in TypeScript and ships plain JavaScript builds. The README states it runs on the browser and in Node.js with ES5 and ES6 support, carries no runtime dependencies, and keeps the codebase under 200 lines of code. The dependency list in package.json is dev only, holding test, lint, typecheck and bundling tooling such as vitest, eslint and vite.

Operation costs are documented as logarithmic for push and pop, constant for peek in the general case, and linear for creating a queue from a pre-existing list of priorities. Those figures follow from the heap layout, and they are the reason a priority queue beats sorting on every element when only the next item matters.

Installing heapify and pushing the first entry

The project is published to npm as heapify, so installation is a normal package install:

sh
npm i heapify

The same package works with yarn:

sh
yarn add heapify

In a Node.js project you can import the constructor directly:

js
import {MinQueue} from "heapify";

A CommonJS project uses require() instead, and package.json exposes both entry points, with heapify.mjs for ES module imports and heapify.js for require(). The first useful example pushes two entries, where the first argument is the key you want back later and the second is the priority used for ordering:

js
const queue = new MinQueue();
queue.push(1, 10);
queue.push(2, 5);
queue.pop();  // 2
queue.peek();  // 1
queue.clear();
queue.pop();  // undefined

MinQueue pops the smallest priority first, so the entry pushed with priority 5 comes out before the one pushed with priority 10. Peek returns the same key without removing it, and popping from an emptied queue returns undefined rather than throwing.

Capacity is fixed at construction and push throws when it is reached

The capacity is the size of the underlying typed arrays backing the heap, and it is set when the queue is constructed. There is no resize step in the documented API. The consequence is concrete: push throws an error if the queue is already at its capacity.

That single line in the API description is the main operational constraint of the library. A worker that drains an unpredictable upstream queue has to size the heap for its worst case backlog up front, because crossing the limit produces an exception at the push site rather than a slower queue or a rejected item. The related read-only capacity property tells you where that limit sits:

js
const queue = new MinQueue(32);
queue.capacity;  // 32

The size property tracks the current occupancy separately, so you can watch how close the queue is to its ceiling:

js
const queue = new MinQueue();
queue.size;  // 0
queue.push(1, 10);
queue.size;  // 1
queue.pop();
queue.size;  // 0

For queues with a bounded, known workload this is a feature, since the memory footprint is fixed at allocation time. For anything bursty, it is a trap that only shows up under load.

Equal priorities have no defined pop order

The heap implementation is not stable, and the API notes say so plainly: if multiple keys have the same priority, there are no guarantees about the order in which they will be popped.

This matters more often than it first appears. Priority queues are commonly used to sequence work, and stable behaviour is usually the expected semantics of a queue, even when the priorities are equal. Two tasks with priority 5 might come out in either order. If a scheduler relies on first-in-first-out among equal-priority work, the tie-break has to come from somewhere else: a monotonic counter encoded into the priority, or a wrapping scheme where the key carries the sequence number.

Note the workaround has a cost. Encoding a counter into the priority changes the value space, and priorities here are unsigned integers bounded by the backing array type. For most scheduling work the practical answer is simpler: make the priority values distinct by construction, and treat the documented instability as a reason not to rely on order rather than as a bug to work around.

The same design also explains why the library does not expose a comparator function. Ordering is fixed at the priority level, not at an arbitrary comparison callback.

What the published benchmark table does and does not prove

The README publishes a comparison against Closure, FastPQ, FlatQueue and TinyQueue, with times in milliseconds for build, push, pop, push/pop batch, push/pop interleaved and push/pop random. The table is marked as generated from commit 97ff2c2 on 2026-03-04, and the project states it is the fastest publicly available JavaScript implementation of a priority queue.

On the numbers shown, Heapify records 9 for push and 48 for pop, against 66 and 286 for Closure, 26 and 327 for TinyQueue, and 13 and 60 for FastPQ. On interleaved push/pop the gap widens to 13 for Heapify against 23 for FastPQ. FlatQueue has no build figure in the table.

Read this as a self-published measurement rather than neutral ground truth. It comes from one commit, on one machine, with a harness that lives in the repository's benchmark directory and scripts/bench.sh. It tells you what the author measured and when, and it points at a real design advantage: two parallel typed arrays with no object allocation per node. It does not tell you how the library behaves on your workload, your runtime or your key distribution, so treat the numbers as a hint about the design and verify the ordering behaviour and memory profile in your own code.

clear() resets the length counter and keeps the memory

clear() empties the queue without touching the heap capacity and without erasing any elements. The documentation describes it as a cheap operation because only the variable tracking the length is reset to zero.

That behaviour is a direct consequence of the typed array design. The backing memory stays allocated and reusable, so a long-lived queue that is drained and refilled does not pay for reallocation on every cycle. The trade-off is that a cleared queue still holds whatever the arrays were sized for, and any references the caller held to popped keys are unaffected.

js
const queue = new MinQueue();
queue.push(1, 10);
console.info(queue.size);  // 1
queue.clear();
console.info(queue.size);  // 0

For a request handler or a game loop that creates a queue per frame and throws it away, clear() on a reused instance avoids repeated allocation. For a long-running process that occasionally needs a small queue, constructing a fresh MinQueue is simpler, since the default capacity of 64 entries is small enough that holding the memory is rarely the problem.

Picking the backing array types in the constructor

The constructor accepts the capacity, optional pre-existing key and priority arrays, and the array types used for each side:

js
const queue1 = new MinQueue(32);
const queue2 = new MinQueue(16, [], [], Uint16Array, Uint32Array);

The second call keeps the capacity at 16, skips both pre-existing arrays by passing empty ones, and stores keys in a Uint16Array while priorities stay in a Uint32Array. Narrowing the key array halves the memory per key compared with the default, at the cost of a 65535 ceiling on key values.

The same trade-off applies to priorities. Since the default is Uint32Array for both sides, keys and priorities larger than 4294967295 are not representable without changing the array type, and negative priorities are outside the unsigned range entirely. If your workload uses negative or fractional priorities, this library does not fit as configured.

The constructor also accepts pre-existing keys and priorities, which lets a queue be seeded in linear time instead of pushing one element at a time. The number of priorities must match the number of keys.

Maintenance state, version history and licensing

The repository is not archived and the last push was on 2026-06-12, so the project is receiving changes, though at a pace of a few months between visible touches rather than a release train. There are no GitHub releases, and package.json declares version 1.0.2, which means there is no tagged release history on the platform to point at. A CHANGELOG.md file exists at the top level of the repository.

The build and release tooling lives in package.json scripts, including build:prod, test, integration, bench and typecheck commands, so the project is developed with a standard Node.js toolchain rather than a bespoke one.

The licence is MIT, which is a permissive choice that lets you use the library in commercial and closed-source products without a copyleft obligation. That is a licensing fact rather than legal advice, and if your organisation requires a specific licence review, the LICENSE.md file at the repository root is the place to start.

One caveat for evaluation: the README does not document a support policy or a compatibility matrix for JavaScript engines and Node.js versions, so treat the ES5 and ES6 support claim as the compatibility statement on offer.

Editorial conclusion

heapify fits job scheduling, pathfinding and event ordering code that lives in Node.js or the browser and needs a priority queue without pulling in a dependency. It is a poor fit if you need a stable ordering for equal priorities, keys or priorities outside the unsigned integer range, or a queue that grows past the capacity you passed to the constructor. Before adopting it, check the capacity sizing against your peak backlog, because push throws an error rather than growing the heap.

Frequently asked questions

How do I install heapify and import MinQueue in a Node.js project?

Install the package with npm i heapify, then import the constructor with import {MinQueue} from "heapify" in an ES module, or use const {MinQueue} = require("heapify") in a CommonJS project. package.json exposes heapify.mjs for ES module imports and heapify.js for require.

What happens if I push more items to heapify than the queue can hold?

push throws an error if the queue is already at its capacity, because the backing typed arrays are fixed at construction. Size the queue with the capacity argument up front and watch the size and capacity properties to see how close you are to the limit.

Does heapify return items with equal priorities in the order they were pushed?

No. The heap implementation is not stable, and the API notes state that there are no guarantees about the order in which keys with the same priority are popped. If you need a fixed tie-break, encode a sequence number into the priority.

How do I empty a heapify queue without allocating a new one?

Call clear(). It resets the tracked length to zero and leaves the heap capacity and the backing arrays untouched, so the same instance can be reused without reallocating memory.

Official sources

  1. Issues
  2. License: MIT
  3. luciopaiva/heapify on GitHub
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/luciopaiva-heapify.svg)](https://hysenlabs.com/projects/luciopaiva-heapify)

Was this useful?