npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zakkster/lite-fastbit32

v1.2.0

Published

Zero-GC, monomorphic 32-bit flag manager and ECS masking primitive for high-performance game loops.

Readme

@zakkster/lite-fastbit32

npm version npm bundle size npm downloads npm total downloads TypeScript Dependencies License: MIT

Zero-GC, monomorphic, branchless 32-bit flag manager for ECS masking, object pools, and 60fps hot-path engine code. Zero dependencies. One class. The fastest 32-bit flag engine in JavaScript.

🚀 Built with Lite-FastBit32: Lite-Tween Pro

If you are building high-performance WebGL applications or JavaScript games, check out Lite-Tween Pro. It is a commercial, zero-allocation ECS tweening engine built directly on top of this bitwise architecture. It completely bypasses the JavaScript Garbage Collector to guarantee a flat memory profile and a stable 120fps on mobile devices.

👉 Get the Lite-Tween Pro Source Code here

(Are you a solo indie dev or student? Reach out to me directly at [email protected] and I will hook you up with an indie discount!)

Why lite-fastbit32?

| Feature | lite-fastbit32 | FastBitSet | TypedFastBitSet | |-----------------------|----------------|------------|-----------------| | Max bits | 32 | Unlimited | Unlimited | | Zero-GC | Yes | No | No | | Monomorphic | Yes | No | No | | Branchless | Yes | No | No | | O(1) popcount | Yes | No | No | | O(1) lowest/highest | Yes | No | No | | O(k) iteration | Yes | No | No | | BitMapper | Yes | No | No | | BigInt support | No | No | No | | ECS-ready | Yes | Yes | Yes | | Object pool scan | Yes | No | No | | Serialization | Yes | Yes | Yes | | Bundle size | < 1KB | ~8KB | ~6KB |

FastBit32 is an engine primitive, not a general-purpose bitfield.

Installation

npm install @zakkster/lite-fastbit32

Quick Start

import { FastBit32, BitMapper } from '@zakkster/lite-fastbit32';

const flags = new FastBit32();

flags.add(1).add(4);         // Set bits 1 and 4
flags.has(4);                 // true
flags.count();                // 2 — O(1) popcount
flags.lowest();               // 1 — O(1) bit-scan forward
flags.remove(1);              // Clear bit 1
flags.serialize();            // Raw uint32 for storage

// Human-readable flag management
const mapper = new BitMapper(['Physics', 'Render', 'AI', 'Input']);
const entity = new FastBit32();
entity.add(mapper.get('Physics')).add(mapper.get('Input'));
mapper.getActiveNames(entity); // ['Physics', 'Input']

The Bit Pipeline

Monomorphic V8 Optimization

FastBit32 stores all state in a single value property — a plain unsigned 32-bit integer. V8's inline cache sees one hidden class for the entire lifetime of the object. Every method is a single bitwise operation on that integer. No arrays. No objects. No allocations. No branches.

The constructor enforces unsigned 32-bit via >>> 0 on the first tick, locking V8 into its fastest integer representation path.

O(1) Popcount (Hamming Weight)

Counting active bits uses the Hacker's Delight parallel bit-count algorithm:

v = v - ((v >>> 1) & 0x55555555)
v = (v & 0x33333333) + ((v >>> 2) & 0x33333333)
result = Math.imul((v + (v >>> 4)) & 0x0F0F0F0F, 0x01010101) >>> 24

Five operations, zero loops, zero branches. Works for any value of the 32-bit integer.

O(1) Bit-Scan (lowest / highest)

Finding the lowest set bit uses isolation + Count Leading Zeros:

lowest = Math.clz32(value & -value) ^ 31

value & -value isolates the lowest set bit into a power-of-two. Math.clz32 counts leading zeros from the left, and XOR 31 converts it to a right-indexed position. One expression, no loops.

highest uses 31 - Math.clz32(value) directly.

O(k) Iteration

All iteration helpers visit only active bits using the v &= v - 1 trick to clear the lowest bit each step. Complexity is O(k) where k is the number of set bits — not 32.

Caveats

  • Silent wraparound: JS bitwise shifts apply modulo 32. add(32) evaluates as add(0). add(40) evaluates as add(8).
  • Truncation: Floats and negatives are silently coerced to unsigned 32-bit integers. -1 >>> 0 becomes 4294967295.
  • Sanitize inputs upstream if your domain logic requires strict bounds.

Benchmark Results

Tested on Apple M2 Pro, Node 22, V8 12.x. All values in ops/ms.

Single-Bit Operations

| Operation | lite-fastbit32 | FastBitSet | TypedFastBitSet | Raw bitwise | |-------------|----------------|------------|------------------|-------------| | set bit | ~240k | ~150k | ~220k | ~260k | | has bit | ~260k | ~180k | ~240k | ~280k | | remove bit | ~240k | ~140k | ~200k | ~260k |

Mask Operations

| Operation | lite-fastbit32 | FastBitSet | |-----------|----------------|------------| | hasAll | ~300k | ~40k | | hasAny | ~300k | ~45k | | hasNone | ~300k | ~45k |

Popcount

| Operation | lite-fastbit32 | FastBitSet | |-----------|----------------|------------| | count | ~350k | ~25k |

Bit-Scan (lowest / highest)

| Operation | lite-fastbit32 | FastBitSet | |-----------|----------------|------------| | lowest | ~350k | N/A | | highest | ~350k | N/A |

lite-fastbit32 is the only library with O(1) bit-scan forward/backward.


Recipes

import { FastBit32 } from '@zakkster/lite-fastbit32';

const POSITION  = 0;
const VELOCITY  = 1;
const SPRITE    = 2;
const COLLISION = 3;
const AI        = 4;

const PHYSICS_QUERY = (1 << POSITION) | (1 << VELOCITY) | (1 << COLLISION);
const RENDER_QUERY  = (1 << POSITION) | (1 << SPRITE);

const entity = new FastBit32();
entity.add(POSITION).add(VELOCITY).add(SPRITE).add(COLLISION);

if (entity.hasAll(PHYSICS_QUERY)) runPhysics(entity);
if (entity.hasAll(RENDER_QUERY))  drawSprite(entity);

Bit 31 (Sign Bit) Warning: In JavaScript, 1 << 31 evaluates to -2147483648 — a negative number. FastBit32 handles this correctly under the hood, but if you log raw mask values to the console, you will see negative integers and assume a bug. This also affects serialize(): masks using bit 31 produce negative numbers in JSON. Recommendation: Keep ECS component indices to 0–30 (31 components). If you must use all 32, compare serialized values with >>> 0 to force unsigned representation.

import { FastBit32 } from '@zakkster/lite-fastbit32';

const pool = new FastBit32();
const objects = new Array(32);

function allocate() {
    const slot = pool.nextClearBit();   // O(1), zero allocation
    if (slot === -1) return null;        // Pool full
    pool.add(slot);
    return slot;
}

function release(slot) {
    pool.remove(slot);
}

allocate(); // 0
allocate(); // 1
release(0);
allocate(); // 0 — immediately reused

Before v1.2.0 this required constructing a scratch FastBit32 from the inverted mask and calling .lowest() on it — one allocation per allocate() call. nextClearBit collapses that to a single Math.clz32 on an inverted-and-isolated bit, with no allocations.

import { FastBit32 } from '@zakkster/lite-fastbit32';

const KEY_LEFT  = 0;
const KEY_RIGHT = 1;
const KEY_JUMP  = 2;
const KEY_FIRE  = 3;

const input = new FastBit32();

window.addEventListener('keydown', e => {
    if (e.code === 'ArrowLeft')  input.add(KEY_LEFT);
    if (e.code === 'ArrowRight') input.add(KEY_RIGHT);
    if (e.code === 'Space')      input.add(KEY_JUMP);
});

window.addEventListener('keyup', e => {
    if (e.code === 'ArrowLeft')  input.remove(KEY_LEFT);
    if (e.code === 'ArrowRight') input.remove(KEY_RIGHT);
    if (e.code === 'Space')      input.remove(KEY_JUMP);
});

if (input.has(KEY_JUMP)) jump();
if (input.hasAny((1 << KEY_LEFT) | (1 << KEY_RIGHT))) move();
import { FastBit32 } from '@zakkster/lite-fastbit32';

const IDLE       = 0;
const RUNNING    = 1;
const JUMPING    = 2;
const ATTACKING  = 3;
const INVINCIBLE = 4;

const state = new FastBit32();
state.add(IDLE);

function startAttack() {
    state.clear().add(ATTACKING).add(INVINCIBLE);
}

function endAttack() {
    state.clear().add(IDLE);
}

console.log(state.count()); // 1 — proof of mutex
import { FastBit32, BitMapper, forEachMapped, forEachMappedObject } from '@zakkster/lite-fastbit32';

const components = new BitMapper(['Position', 'Velocity', 'Sprite', 'AI']);
const entity = new FastBit32();
entity.add(components.get('Position')).add(components.get('AI'));

console.log(components.getActiveNames(entity)); // ['Position', 'AI']

forEachMapped(entity, components, (name, bit) => {
    console.log(`Component ${name} at bit ${bit}`);
});

const systems = { Position: updatePos, Velocity: updateVel, Sprite: draw, AI: think };
forEachMappedObject(entity, components, systems, (system, name) => {
    system(entity);
});
import { FastBit32, forEachMaskPair, forEachMaskDiff, forEachMaskUnion } from '@zakkster/lite-fastbit32';

const required = new FastBit32().add(0).add(1).add(3);
const available = new FastBit32().add(0).add(3).add(5);

forEachMaskPair(required, available, bit => console.log('matched:', bit));
// matched: 0, matched: 3

forEachMaskDiff(required, available, bit => console.log('missing:', bit));
// missing: 1

forEachMaskUnion(required, available, bit => console.log('all:', bit));
// all: 0, all: 1, all: 3, all: 5

API

new FastBit32(initial?)

| Parameter | Type | Default | Description | |---|---|---|---| | initial | number | 0 | Starting bitmask. Coerced to unsigned 32-bit via >>> 0. |

Single Bit Operations

| Method | Returns | Description | |---|---|---| | .add(bit) | this | Set bit at position (0–31). | | .remove(bit) | this | Clear bit at position (0–31). | | .toggle(bit) | this | Flip bit at position (0–31). | | .has(bit) | boolean | Test if bit is active. |

Bulk Mask Operations

| Method | Returns | Description | |---|---|---| | .hasAll(mask) | boolean | True if all bits in mask are active. | | .hasAny(mask) | boolean | True if any bit in mask is active. | | .hasNone(mask) | boolean | True if no bits in mask are active. |

In-Place Mutations

| Method | Returns | Description | |---|---|---| | .clear() | this | Reset all 32 bits to zero. | | .union(mask) | this | Bitwise OR — add all bits in mask. | | .difference(mask) | this | Bitwise AND NOT — remove all bits in mask. | | .intersect(mask) | this | Bitwise AND — keep only bits present in both. |

Advanced Helpers

| Method | Returns | Description | |---|---|---| | .count() | number | O(1) popcount — number of active bits (0–32). | | .countMasked(mask) | number | O(1) popcount within a masked region. | | .countRange(start, end) | number | O(1) popcount within inclusive range [start, end]. | | .lowest() | number | O(1) index of least significant active bit. -1 if empty. | | .highest() | number | O(1) index of most significant active bit. -1 if empty. | | .nextClearBit() | number | O(1) index of least significant clear bit. -1 if full. Zero-alloc pool slot lookup. | | .highestClearBit() | number | O(1) index of most significant clear bit. -1 if full. | | .isEmpty() | boolean | True if value is 0. | | .isFull() | boolean | True if all 32 bits are active. |

Iteration

| Method | Returns | Description | |---|---|---| | .forEach(callback) | this | O(k) iteration over active bits. callback(bit). |

Utility

| Method | Returns | Description | |---|---|---| | .clone() | FastBit32 | Independent copy. Mutations do not propagate. | | .serialize() | number | Export raw uint32 for JSON/binary storage. | | FastBit32.deserialize(n) | FastBit32 | Restore from a serialized uint32. |

Debug & Init Helpers

These methods allocate. Do not call them inside hot loops.

| Method | Returns | Description | |---|---|---| | .toBinaryString(padded?) | string | 32-char binary representation, LSB on the right. Sign-bit safe. | | .toArray() | number[] | Active bit indexes in ascending order. Inlined O(k) — no closure allocation. | | .fromArray(bits) | this | Replaces the mask from a bit-index array. Init/deserialization only. |

new BitMapper(names?)

| Method | Returns | Description | |---|---|---| | .get(name) | number | Bit index for a flag name. Throws if unknown. | | .getMask(names) | number | Combined uint32 mask from flag name array. | | .getActiveNames(fb32) | string[] | Active flag names from a FastBit32 instance. | | .getName(bit) | string \| undefined | O(1) reverse lookup — bit index to name. |

Standalone Iteration Helpers

| Function | Description | |---|---| | forEachArray(mask, array, cb) | cb(element, bit) for each active bit. | | forEachObject(mask, keys, obj, cb) | cb(value, key, bit) via keys array. | | forEachMapped(mask, mapper, cb) | cb(name, bit) via BitMapper. | | forEachMappedObject(mask, mapper, obj, cb) | cb(value, key, bit) via BitMapper + object. | | forEachMaskPair(maskA, maskB, cb) | cb(bit) for intersection (A & B). | | forEachMaskDiff(maskA, maskB, cb) | cb(bit) for difference (A & ~B). | | forEachMaskUnion(maskA, maskB, cb) | cb(bit) for union (A | B). |


Changelog

v1.2.0

New: Clear-bit scansnextClearBit() and highestClearBit(). O(1) bit-scan on the inverted mask via Math.clz32. The object-pool slot-lookup pattern is now truly zero-allocation; the prior new FastBit32(~pool.value & 0xFFFFFFFF).lowest() workaround is retired.

New: isFull() — Companion to isEmpty(). Uses ~this.value === 0 for correctness across both signed (-1) and unsigned (0xFFFFFFFF) representations of an all-set int32.

New: countRange(start, end) — O(1) popcount within an inclusive bit range. Mask is built with >>> to sidestep the 1 << 32 wraparound.

New: Debug & init helperstoBinaryString(padded?), toArray(), fromArray(bits). Clearly documented as allocating; kept outside the hot-path API surface. toBinaryString forces unsigned via >>> 0 so bit 31 does not trigger toString(2)'s minus-sign formatting. toArray inlines the v &= v - 1 loop rather than delegating to forEach, avoiding a per-call closure allocation. fromArray replaces the current value (does not OR into it) and writes this.value exactly once.

v1.1.0

New: BitMapper — Human-to-hardware bridge. Maps semantic string names to bit indices and masks, with O(1) reverse lookup via getName(bit).

New: forEach(callback) — O(k) iteration on FastBit32 instances. Visits only active bits in ascending order using v &= v - 1 bit-clearing. Returns this for chaining.

New: 7 standalone iteration helpersforEachArray, forEachObject, forEachMapped, forEachMappedObject, forEachMaskPair, forEachMaskDiff, forEachMaskUnion. All O(k). Connect masks directly to arrays, objects, BitMapper dictionaries, and mask set operations without intermediate allocations.

v1.0.0

Initial release. FastBit32 core: single-bit ops, bulk mask ops, in-place set math, O(1) popcount, O(1) bit-scan (lowest/highest), clone, serialize/deserialize.


License

MIT

Part of the @zakkster ecosystem

Zero-GC, deterministic, tree-shakeable micro-libraries for high-performance web applications.