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.0.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.

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 | | 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 } 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

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.

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';

// Bit = 1 means slot is occupied
const pool = new FastBit32();
const objects = new Array(32);

function allocate() {
    // Invert to find free slots, mask to pool size
    const free = new FastBit32(~pool.value & 0xFFFFFFFF);
    const slot = free.lowest();

    // ⚠️ CRITICAL: Always check for -1.
    // If the pool is full, lowest() returns -1.
    // Accessing objects[-1] bypasses V8's array bounds optimization,
    // triggering a dictionary-mode fallback on the entire array —
    // a massive de-optimization penalty that persists for the array's lifetime.
    if (slot === -1) return null; // Pool exhausted — expand or drop

    pool.add(slot);
    return slot;
}

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

allocate(); // 0
allocate(); // 1
release(0);
allocate(); // 0 — immediately reused
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);
});

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

const LAYER_PLAYER    = 0;
const LAYER_ENEMY     = 1;
const LAYER_BULLET    = 2;
const LAYER_WALL      = 3;
const LAYER_PICKUP    = 4;

const playerMask = new FastBit32();
playerMask.add(LAYER_ENEMY).add(LAYER_PICKUP).add(LAYER_WALL);

const bulletMask = new FastBit32();
bulletMask.add(LAYER_ENEMY).add(LAYER_WALL);

function canCollide(entityLayer, targetMask) {
    return targetMask.has(entityLayer);
}
import { FastBit32 } from '@zakkster/lite-fastbit32';

const FLAG_RELIABLE   = 0;
const FLAG_ORDERED    = 1;
const FLAG_COMPRESSED = 2;
const FLAG_ENCRYPTED  = 3;

const packet = new FastBit32();
packet.add(FLAG_RELIABLE).add(FLAG_ENCRYPTED);

const raw = packet.serialize();  // Send as uint32
// ... network transport ...
const restored = FastBit32.deserialize(raw);
if (restored.has(FLAG_RELIABLE)) ack(packet);

In a strict FSM, only one state should be active at any time. Never use remove(OLD).add(NEW) — if the remove and add target the same bit index by mistake, you silently corrupt the state. Use .clear().add() to guarantee zero overlapping bits.

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);

// ✅ CORRECT — mutex transition: clear ALL bits, then set the new state.
// Guarantees zero overlap regardless of previous state.
function startAttack() {
    state.clear().add(ATTACKING).add(INVINCIBLE);
}

// ✅ CORRECT — return to single state after compound state ends.
function endAttack() {
    state.clear().add(IDLE);
}

// ❌ WRONG — remove/add leaves stale bits if you forget one:
// state.remove(ATTACKING).remove(INVINCIBLE).add(IDLE);
// If INVINCIBLE was already cleared by another system, this still "works"
// but masks a logic error. clear() is unconditional and safe.

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

// Save
const entities = [entityA.serialize(), entityB.serialize()];
const json = JSON.stringify(entities); // "[18, 7]" — bytes, not objects

// Load
const restored = JSON.parse(json).map(FastBit32.deserialize);

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. | | .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. | | .isEmpty() | boolean | True if value is 0. |

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. |


License

MIT

Part of the @zakkster ecosystem

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