The sawtooth problem

When a web app or game starts running poorly after some time, the cause often isn't visible in the code. Opening Chrome's memory profiling tools tells a different story: a repeated sawtooth pattern in the memory timeline. Each tooth marks a garbage collection (GC) pulse—the moment the runtime reclaims memory from objects your code has abandoned. Frequent GC events steal execution time from your application and degrade performance.

JavaScript's memory model delegates allocation and deallocation to a garbage collector rather than to the programmer. You don't explicitly free objects; the GC decides when reclaiming memory is worth the cost. That decision process requires the GC to pause execution while it analyzes and cleans up, and you have no control over when it runs or how long it takes. For high-performance applications that depend on consistent frame times, that unpredictability is a problem.

The fix is to reduce "memory churn"—the constant creation and release of objects—so the GC has less reason to run. In an ideal profile, memory grows sharply at startup, then levels off instead of cycling up and down as objects are allocated and collected.

Static-memory JavaScript with object pools

Static-memory JavaScript is the practice of pre-allocating all the memory your application will need for its lifetime, then managing those objects manually. The approach is two-fold:

  1. Instrument your app to find the maximum number of live objects you need, per type, across realistic usage scenarios.
  2. Pre-allocate that maximum up front and recycle objects as they become unused, rather than allocating fresh from the memory heap.

Object pooling is the mechanism that makes this possible. A pool holds a set of unused objects of a given type. When your code needs a new object, you pull one from the pool instead of creating one. When you're done with it, you return it to the pool rather than dereferencing it. Because pooled objects are never fully released, the GC never collects them, and control over memory returns to the programmer.

Since applications use many object types, you'll want one pool per type that experiences high churn:

var newEntity = gEntityObjectPool.allocate();
newEntity.pos = {x: 215, y: 88};

//..... do some stuff with the object that we need to do

gEntityObjectPool.free(newEntity); //free the object when we're done
newEntity = null; //free this object reference

In practice, you'll level off at some maximum number of live objects. Run your application through various usage scenarios, record the peak requirement per type, and pre-fill your pools to that amount during initialization. That shifts the allocation cost to startup and minimizes dynamic allocations during execution:

function init() {
  //preallocate all our pools. 
  //Note that we keep each pool homogeneous wrt object types
  gEntityObjectPool.preAllocate(256);
  gDomObjectPool.preAllocate(888);
}

The amount you pre-allocate depends on your application's behavior. The theoretical maximum isn't always the best choice; for instance, choosing an average maximum can keep the memory footprint smaller for users who never hit peak usage.

Trade-offs

Object pooling isn't a universal win. It fits applications where static memory growth patterns are acceptable, but it has drawbacks. Retaining unused objects means memory stays occupied even when the app is idle. And manually tracking object lifetimes adds complexity; a forgotten return-to-pool is the functional equivalent of a leak. Pooling also forces you to think carefully about ownership—once an object is returned to a pool, external references to it become invalid.

Still, for HTML5 games and other frame-rate-sensitive applications, the benefit is clear. By measuring your app's memory usage and introducing pools for high-churn types, you can remove the GC from the critical path and reclaim execution time for rendering and gameplay.