A lightweight TypeScript/JavaScript library that bridges C-style structs into modern JS runtimes.
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.
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.
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).
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.
npm install @megaofmegalodon/stackjsimport { 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();
}StackJS.register<S extends Schema>(schema: S, defaultValues?: StructValues<S>): CompiledStruct<S>- Creates a reusable struct schema.
StackJS.fromKB(kilobytes: number): StackJSStackJS.fromMB(megabytes: number): StackJSStackJS.fromGB(gigabytes: number): StackJSnew StackJS(sizeInBytes: number, endianness?: "LE" | "BE")
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.
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.
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);
}
}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
};This project is licensed under MIT.