Skip to content

Repository files navigation

StackJS

A lightweight TypeScript/JavaScript library that bridges C-style structs into modern JS runtimes.

Why StackJS?

1. The Problem: Short-Lived Objects in Hot Loops

In high-frequency JS environments, you constantly need temporary objects for calculations:

function updateParticle(p, d) {
  // Creating short-lived vector objects every single frame
  const velocity = { x: p.vx + d.x ** d.mlt, y: p.vy + d.y ** d.mlt };
  const step = scaleVector(velocity, 0.016);
  p.x += step.x;
}

Creating thousands of small, short-lived objects per second forces V8's garbage collector into overdrive and triggers periodic Garbage Collection (GC). This causes stutters, frame drops, and latency spikes.

2. The Flaw in Current Solutions

Engineers typically try two workarounds to avoid GC pressure, but both introduce major drawbacks:

  • Object Pooling: Preserving a fixed pool of JavaScript objects eliminates allocations, but it creates reference management hell.
    • If a developer forgets to return an object to the pool, memory leaks.
    • If an object is silently used twice, data gets corrupted across systems.
  • Pre-Allocated TypedArrays: Using raw binary buffers avoids the GC, but manual index management quickly degenerates into unreadable, bug-prone "magic number" math.

3. What StackJS Solves

StackJS gives you the pure performance of pre-allocated binary memory with clean ergonomics that mimic C-structs:

  • CPU Cache Locality: All data is stored as raw values inside a single contiguous ArrayBuffer, drastically improving cache efficiency compared to normal JS objects.
  • Zero Allocation: Allocating and deallocating structs cause zero GC overhead.
  • Readable Struct Schemas: Define named fields and primitive types, eliminating "magic number" array math while preserving strict byte-alignment padding.
  • Globally Usable: StackJS isn't locked to a single global memory heap. This allows you to instantiate as many StackJS instances as your program needs (e.g. one for playerBuffer, buildingBuffer, projectileBuffer, shortLivedObjects, etc).

What StackJS is not

StackJS is not intended to replace every single JS object.

Normal JS objects still remain the right choice for complex or dynamic data. StackJS is designed to replace hard to manage buffers and short-lived objects for programs where avoiding GC for consistent code runtime is the goal.

Installation

npm install @megaofmegalodon/stackjs

Quick Start

import { StackJS } from "@megaofmegalodon/stackjs";

// 1. Pre-allocate a 4 MB contiguous memory stack
const stack = StackJS.fromMB(4);

// 2. Define a C-style struct layout
const Particle = StackJS.register({
    x: "f32",
    y: "f32",
    vx: "f32",
    vy: "f32",
    active: "u8",
});

function updateParticleScope() {
  const ptr = stack.allocF(Particle);

  stack.set(ptr, Particle.X, 10.0);
  stack.set(ptr, Particle.Y, 20.0);
  stack.set(ptr, Particle.VX, 1.5);
  stack.set(ptr, Particle.VY, -0.5);

  const currentX = stack.get(ptr, Particle.X);
  const vx = stack.get(ptr, Particle.VX);
  stack.set(ptr, Particle.X, currentX + vx);

  stack.pop();
}

// 3. Run hot-loop: zero short-lived objects allocated
for (let i = 0; i < 10000; i++) {
  updateParticleScope();
}

API Reference

Struct Schema Registry

  • StackJS.register<S extends Schema>(schema: S, defaultValues?: StructValues<S>): CompiledStruct<S> - Creates a reusable struct schema.

StackJS Constructors

  • StackJS.fromKB(kilobytes: number): StackJS
  • StackJS.fromMB(megabytes: number): StackJS
  • StackJS.fromGB(gigabytes: number): StackJS
  • new StackJS(sizeInBytes: number, endianness?: "LE" | "BE")

Scope & Allocation

  • allocFrame(): void - Pushes a new activation record onto the scope stack.
  • alloc(frame: StructFrame): StackPointer - Allocates memory for a struct within the active frame.
  • popFrame(): void - Frees the top activation frame.
  • allocF(frame: StructFrame): void - A shorthand for allocFrame() and alloc() that pushes a new activation record and allocates memory for a struct in the new frame.
  • pop(): void - A shorthand for popFrame(): void.

Data Access

  • set(ptr: StackPointer, field: FieldData, val: number): void - Writes a scalar numeric value into stack buffer memory.
  • get(ptr: StackPointer, field: FieldData): number - Reads a scalar numeric value from stack buffer memory.
  • ext<T>(ptr, frame, TypedArrayConstructor): T - Returns a TypedArray view over the struct.

Use Case Examples:

Buffer Replacement

import { StackJS } from "@megaofmegalodon/stackjs";

const playerStruct = StackJS.register({
    x: "f32",
    y: "f32",
    health: "f32"
}, {
    x: 0.0,
    y: 0.0,
    health: 100.0 
});

// create a 1000 playerStructBuffer that benefits from CPU cache locality
const playerBuffer = new StackJS(playerStruct.byteSize * 1000);
playerBuffer.allocFrame();
for (let i = 0; i < 1000; i++) playerBuffer.alloc(playerStruct);

function updatePlayers(dt) {
    const size = playerBuffer.size;

    for (let i = 0; i < 1000; i++) {
        const ptr = playerStruct.byteSize * i;
        const oldX = playerBuffer.get(ptr, playerStruct.X);
        playerBuffer.set(ptr, playerStruct.X, oldX + dt);
    }
}

Memory Management

import { StackJS } from "@megaofmegalodon/stackjs";

// Creating memory budgets for different systems
const memorySpecs = {
    particles: StackJS.fromMB(4),    // 4MB max for particles
    projectiles: StackJS.fromMB(2),  // 2MB max for bullets
    scratchpad: StackJS.fromKB(256), // 256KB for math vectors
};

License

This project is licensed under MIT.

About

A lightweight JS library that bridges JS and C-style structs.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages