@megaofmegalodon/stackjs
v1.0.0
Published
A lightweight TypeScript/JavaScript library that bridges C-style structs into modern JS runtimes.
Maintainers
Readme
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/stackjsQuick 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): StackJSStackJS.fromMB(megabytes: number): StackJSStackJS.fromGB(gigabytes: number): StackJSnew 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 forallocFrame()andalloc()that pushes a new activation record and allocates memory for a struct in the new frame.pop(): void- A shorthand forpopFrame(): 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.
