Object Pools in JavaScript: When Reusing Objects Beats Letting GC Do the Work
In JavaScript, creating objects inside functions is a very common pattern:
function createMessage(type, payload) {
return {
type,
payload,
timestamp: Date.now()
};
}
for (let i = 0; i < 100000; i++) {
const message = createMessage("event", i);
// use message
}
This code itself is not problematic. But if the function is called at high frequency and each call creates a temporary object, a large number of short-lived objects can be produced. They will frequently enter the garbage collection process, and in some scenarios this can cause:
- Increased memory allocation count;
- Frequent Minor GC;
- Frame stuttering in games or animations;
- Reduced throughput in high-frequency data processing;
- More noticeable performance fluctuation on mobile devices.
An Object Pool is an optimization technique that reduces repeated allocation by "reusing objects". It pre-creates or creates on demand a batch of objects, retrieves them from the pool when needed, and returns them to the pool after use, instead of creating new objects every time.
However, note that modern JavaScript engines already have very efficient garbage collection. Object pools are not suitable for every scenario; they should only be used after performance profiling.
1. The Basic Idea of an Object Pool
Normal approach:
function createVector(x, y) {
return { x, y };
}
for (let i = 0; i < 100000; i++) {
const vector = createVector(i, i);
// use vector
}
Object pool approach:
const vectorPool = [];
function acquireVector() {
return vectorPool.pop() || { x: 0, y: 0 };
}
function releaseVector(vector) {
vector.x = 0;
vector.y = 0;
vectorPool.push(vector);
}
for (let i = 0; i < 100000; i++) {
const vector = acquireVector();
vector.x = i;
vector.y = i;
// use vector
releaseVector(vector);
}
The core flow is:
get object -> set data -> use object -> clear state -> return object
Instead of repeatedly doing:
create object -> use object -> wait for GC to reclaim
The object pool turns a large number of short-lived objects into a small number of reusable objects.
2. A Generic Object Pool Implementation
We can first implement a generic object pool.
class ObjectPool {
constructor(factory, reset, initialSize = 0, maxSize = Infinity) {
this.factory = factory;
this.reset = reset;
this.maxSize = maxSize;
this.items = [];
for (let i = 0; i < initialSize; i++) {
this.items.push(factory());
}
}
acquire() {
return this.items.pop() || this.factory();
}
release(object) {
this.reset(object);
if (this.items.length < this.maxSize) {
this.items.push(object);
}
}
get size() {
return this.items.length;
}
}
Usage example:
const userPool = new ObjectPool(
() => ({
id: 0,
name: "",
score: 0,
active: false
}),
user => {
user.id = 0;
user.name = "";
user.score = 0;
user.active = false;
},
10,
100
);
Acquiring and releasing objects:
const user = userPool.acquire();
user.id = 1001;
user.name = "Tom";
user.score = 90;
user.active = true;
// use user
userPool.release(user);
The reset here is very important. If the object state is not reset, the next time it is acquired, data from the previous use may remain.
3. Why Object State Must Be Reset
Incorrect example:
const pool = [];
function acquireUser() {
return pool.pop() || {
name: "",
age: 0
};
}
function releaseUser(user) {
pool.push(user);
}
const user1 = acquireUser();
user1.name = "Alice";
user1.age = 18;
releaseUser(user1);
const user2 = acquireUser();
console.log(user2);
// { name: "Alice", age: 18 }
user2 inherits the data from user1, which can lead to very subtle business bugs.
Correct approach:
function releaseUser(user) {
user.name = "";
user.age = 0;
pool.push(user);
}
Or reset upon acquisition:
function acquireUser() {
const user = pool.pop() || {
name: "",
age: 0
};
user.name = "";
user.age = 0;
return user;
}
Generally, cleaning up during the release phase is recommended, because the object's state is clearer at the time of return.
4. Use Case 1: Particle Systems in Games
Particle systems typically create, update, and destroy a large number of particles every frame, for example:
- Explosion effects;
- Smoke;
- Fire;
- Rain and snow;
- Bullet trails;
- Magic effects.
If every particle is created with new, a large number of short-lived objects can be produced.
4.1 Without an Object Pool
function createParticle(x, y) {
return {
x,
y,
vx: Math.random() * 2 - 1,
vy: Math.random() * 2 - 1,
life: 1,
size: 4
};
}
const particles = [];
function emitParticle(x, y) {
particles.push(createParticle(x, y));
}
When creating and deleting many particles, a lot of garbage objects are produced.
4.2 With an Object Pool
class ParticlePool {
constructor(size = 1000) {
this.pool = [];
for (let i = 0; i < size; i++) {
this.pool.push(this.create());
}
}
create() {
return {
x: 0,
y: 0,
vx: 0,
vy: 0,
life: 0,
size: 0,
active: false
};
}
acquire(x, y) {
const particle = this.pool.pop();
if (!particle) {
return null;
}
particle.x = x;
particle.y = y;
particle.vx = Math.random() * 2 - 1;
particle.vy = Math.random() * 2 - 1;
particle.life = 1;
particle.size = 4;
particle.active = true;
return particle;
}
release(particle) {
particle.x = 0;
particle.y = 0;
particle.vx = 0;
particle.vy = 0;
particle.life = 0;
particle.size = 0;
particle.active = false;
this.pool.push(particle);
}
}
Particle system:
const particlePool = new ParticlePool(1000);
const activeParticles = [];
function emit(x, y, count) {
for (let i = 0; i < count; i++) {
const particle = particlePool.acquire(x, y);
if (particle) {
activeParticles.push(particle);
}
}
}
function update(deltaTime) {
for (let i = activeParticles.length - 1; i >= 0; i--) {
const particle = activeParticles[i];
particle.x += particle.vx * deltaTime;
particle.y += particle.vy * deltaTime;
particle.life -= deltaTime;
if (particle.life <= 0) {
activeParticles.splice(i, 1);
particlePool.release(particle);
}
}
}
This pattern suits systems where the particle count is relatively controllable, the particle structure is fixed, and the lifecycle is clear.
5. Use Case 2: Bullets and Enemy Objects in Games
Bullets typically have a clear lifecycle:
create bullet -> fly -> hit or go out of range -> destroy
An object pool can avoid frequent creation and destruction.
class BulletPool {
constructor(maxSize = 500) {
this.pool = [];
for (let i = 0; i < maxSize; i++) {
this.pool.push(this.createBullet());
}
}
createBullet() {
return {
x: 0,
y: 0,
speed: 0,
angle: 0,
damage: 0,
active: false
};
}
acquire(config) {
const bullet = this.pool.pop();
if (!bullet) {
return null;
}
bullet.x = config.x;
bullet.y = config.y;
bullet.speed = config.speed;
bullet.angle = config.angle;
bullet.damage = config.damage;
bullet.active = true;
return bullet;
}
release(bullet) {
bullet.active = false;
bullet.x = 0;
bullet.y = 0;
bullet.speed = 0;
bullet.angle = 0;
bullet.damage = 0;
this.pool.push(bullet);
}
}
Usage:
const bulletPool = new BulletPool(500);
const bullets = [];
function fire() {
const bullet = bulletPool.acquire({
x: 100,
y: 200,
speed: 800,
angle: 0,
damage: 10
});
if (bullet) {
bullets.push(bullet);
}
}
In games, you can also use a "fixed array + active flag" instead of push and splice to reduce array operations:
const bullets = Array.from({ length: 500 }, () => ({
x: 0,
y: 0,
active: false
}));
function acquireBullet() {
return bullets.find(bullet => !bullet.active);
}
function releaseBullet(bullet) {
bullet.active = false;
}
However, find traverses the array each time; if performance is critical, maintain a free index list or use a dedicated free list.
6. Use Case 3: High-Frequency Math Calculation Objects
In animation, physics calculations, and map rendering, many temporary vectors are often created:
function add(a, b) {
return {
x: a.x + b.x,
y: a.y + b.y
};
}
If this function is in a high-frequency loop, it can produce many temporary objects.
6.1 Avoiding Allocation via Output Parameters
One simple approach is to not return a new object, but write the result into a specified object:
function addTo(out, a, b) {
out.x = a.x + b.x;
out.y = a.y + b.y;
return out;
}
const result = { x: 0, y: 0 };
addTo(result, { x: 1, y: 2 }, { x: 3, y: 4 });
console.log(result);
// { x: 4, y: 6 }
This approach is usually simpler than an object pool and makes object lifecycle easier to control.
6.2 Using a Vector Object Pool
const vectorPool = new ObjectPool(
() => ({ x: 0, y: 0 }),
vector => {
vector.x = 0;
vector.y = 0;
},
32,
128
);
function addVector(a, b) {
const result = vectorPool.acquire();
result.x = a.x + b.x;
result.y = a.y + b.y;
return result;
}
const result = addVector(
{ x: 10, y: 20 },
{ x: 5, y: 8 }
);
console.log(result);
vectorPool.release(result);
But pay special attention: the caller must clearly know when to release result. If business code cannot reliably manage the lifecycle, using output parameters is usually safer.
7. Use Case 4: Network Protocols and Log Processing
In WebSocket, message queues, log parsing, and similar scenarios, a large number of messages may be processed per second:
function parseMessage(raw) {
return {
type: raw.type,
id: raw.id,
payload: raw.payload,
receivedAt: Date.now()
};
}
If message processing is very frequent, message objects can be reused:
const messagePool = new ObjectPool(
() => ({
type: "",
id: 0,
payload: null,
receivedAt: 0
}),
message => {
message.type = "";
message.id = 0;
message.payload = null;
message.receivedAt = 0;
},
100,
1000
);
function handleRawMessage(raw) {
const message = messagePool.acquire();
message.type = raw.type;
message.id = raw.id;
message.payload = raw.payload;
message.receivedAt = Date.now();
try {
processMessage(message);
} finally {
messagePool.release(message);
}
}
Using try...finally here ensures that even if an exception is thrown during processing, the object is still returned to the pool.
However, this approach has a limitation: processMessage cannot asynchronously hold the object.
Incorrect example:
function processMessage(message) {
setTimeout(() => {
console.log(message.type);
}, 100);
}
If message has already been returned to the pool and reused before the setTimeout executes, the timer may read data from another message.
For asynchronous scenarios, copy the data, or delay the release:
function processMessageAsync(message) {
const snapshot = {
type: message.type,
id: message.id,
payload: message.payload
};
setTimeout(() => {
console.log(snapshot.type);
}, 100);
}
8. Core Design Principles of an Object Pool
1. Objects must have a clear lifecycle
Objects suitable for an object pool usually follow:
acquire -> use -> release
For example:
- Particles;
- Bullets;
- Temporary vectors;
- Render commands;
- Network messages;
- Short-lived task objects.
Objects not suitable for an object pool usually include:
- Business entities with uncertain lifecycles;
- Objects held for a long time by multiple modules;
- Objects frequently passed asynchronously;
- Objects with complex state that is difficult to fully reset.
2. Do not hold an object for a long time after acquiring it
An object pool object is essentially a "borrowed object".
const item = pool.acquire();
// use item
pool.release(item);
After release, it cannot be used anymore:
pool.release(item);
// Error: item has been returned
console.log(item.name);
A more serious situation:
const cache = [];
const item = pool.acquire();
cache.push(item);
pool.release(item);
At this point, cache still holds an object that has been returned to the pool. When the pool reuses it, the data in cache may change.
3. Clear references to avoid accidental memory retention
If an object holds large arrays, DOM nodes, or complex data, references should be cleared on release:
const taskPool = new ObjectPool(
() => ({
callback: null,
data: null
}),
task => {
task.callback = null;
task.data = null;
}
);
Otherwise the pool itself will continue to hold these references, actually increasing memory usage.
4. Keep object structure stable
JavaScript engines optimize based on object structure. Objects in an object pool should preferably have a consistent property structure:
function createParticle() {
return {
x: 0,
y: 0,
vx: 0,
vy: 0,
life: 0,
active: false
};
}
Avoid dynamically adding or deleting properties like this:
particle.extraData = someData;
delete particle.extraData;
A better approach is to fix the property set:
function createParticle() {
return {
x: 0,
y: 0,
vx: 0,
vy: 0,
life: 0,
active: false,
extraData: null
};
}
A fixed object structure generally helps the JavaScript engine optimize access performance.
9. An Object Pool Does Not Always Improve Performance
Although an object pool can reduce allocation, it also introduces additional costs:
- Acquire and release logic;
- Object state reset;
- Idle objects holding memory for a long time;
- Increased code complexity;
- Difficult lifecycle management;
- Data contamination if used incorrectly.
Modern JavaScript engines have specific optimizations for short-lived objects. Many temporary objects in ordinary code are quickly collected in the young generation garbage collection, and the actual cost may not be high.
For example, the following code may not need an object pool:
function calculate(x, y) {
return {
x: x * 2,
y: y * 2
};
}
If it is not called frequently, or if it is not on a high-frequency path like animation, physics, or rendering, directly creating objects is likely simpler and safer.
10. When an Object Pool Is Appropriate
Typical scenarios suitable for an object pool:
- Creating a large number of objects every frame;
- Objects have very short lifecycles;
- Object structure is fixed;
- Object creation and destruction frequency is very high;
- Acquisition and release can be clearly controlled;
- Performance profiling shows GC is the main bottleneck;
- The application runs on devices with weak memory or CPU.
Common domains include:
- Canvas and WebGL games;
- Particle systems;
- Physics engines;
- Real-time data visualization;
- High-frequency log parsing;
- WebSocket message processing;
- Audio processing;
- Animation systems;
- Large lists or task scheduling.
11. When Not to Use an Object Pool
The following situations are generally not recommended:
1. Object creation frequency is low
const config = {
theme: "dark",
language: "zh-CN"
};
There is no need to put such objects into an object pool.
2. Object lifecycle is complex
When objects are passed between multiple components, cached, or used asynchronously, it is difficult to release them accurately.
3. Object state is complex
If an object has dozens of fields, and some fields have complex dependencies, the reset process can easily miss state.
4. Objects must be persisted for a long time
For example, user information, page state, business entities, etc. These do not fit the "short lifecycle, repeated use" characteristic.
5. No performance testing has been done
Do not blindly use an object pool just because "creating objects triggers GC". First confirm the problem with performance tools.
12. Object Pool vs. Array and TypedArray
If the data structure is very fixed, you might not even use ordinary objects, but use arrays or TypedArray instead.
For example, particle data:
const maxParticles = 1000;
const positionsX = new Float32Array(maxParticles);
const positionsY = new Float32Array(maxParticles);
const velocitiesX = new Float32Array(maxParticles);
const velocitiesY = new Float32Array(maxParticles);
const lifes = new Float32Array(maxParticles);
const active = new Uint8Array(maxParticles);
This "Structure of Arrays" (SoA) has the following characteristics:
- Does not create a large number of objects;
- Data is contiguous, cache-friendly;
- Suitable for batch computation;
- Suitable for WebGL and physics calculations;
- More stable memory layout.
In contrast, an ordinary array of objects is "Array of Structures" (AoS):
const particles = [
{ x: 0, y: 0, life: 1 },
{ x: 10, y: 20, life: 0.5 }
];
When data scales are large and computation is very frequent, TypedArray may be more suitable than an object pool.
13. How to Correctly Evaluate Whether an Object Pool Is Effective
Do not judge based on theory alone; use actual tests.
Focus on:
- Execution time measured with
performance.now(); - The Performance panel in Chrome DevTools;
- Heap changes in the Memory panel;
- Whether GC events are frequent;
- Whether the frame rate is stable;
- Whether memory continuously grows;
- Actual performance on mobile devices.
Simple test:
function benchmark(name, fn, count = 1000000) {
const start = performance.now();
for (let i = 0; i < count; i++) {
fn(i);
}
const end = performance.now();
console.log(`${name}: ${(end - start).toFixed(2)}ms`);
}
Test normal creation:
benchmark("普通对象", i => {
const item = {
x: i,
y: i * 2
};
item.x += 1;
});
Test object pool:
const pool = new ObjectPool(
() => ({ x: 0, y: 0 }),
item => {
item.x = 0;
item.y = 0;
},
100
);
benchmark("对象池", i => {
const item = pool.acquire();
item.x = i;
item.y = i * 2;
item.x += 1;
pool.release(item);
});
Note that simple benchmarks are easily affected by JIT optimization, browser state, and machine load. Best to run multiple rounds and validate with real business scenarios.
14. A More Practical Object Pool Wrapper
You can add usage-state checks to help discover problems like double release or releasing invalid objects:
class SafeObjectPool {
constructor(factory, reset, initialSize = 0, maxSize = Infinity) {
this.factory = factory;
this.reset = reset;
this.maxSize = maxSize;
this.pool = new Set();
for (let i = 0; i < initialSize; i++) {
const object = factory();
object.__inPool = true;
this.pool.add(object);
}
}
acquire() {
const iterator = this.pool.values();
const object = iterator.next().value;
if (object) {
this.pool.delete(object);
object.__inPool = false;
return object;
}
const created = this.factory();
created.__inPool = false;
return created;
}
release(object) {
if (!object || object.__inPool) {
throw new Error("对象无效或已经被释放");
}
this.reset(object);
if (this.pool.size < this.maxSize) {
object.__inPool = true;
this.pool.add(object);
}
}
}
In production, these checks are not necessarily needed, but during development they help find object lifecycle errors.
However, it is not recommended to permanently add debug fields like __inPool to core objects, because it changes the object structure. You can use a WeakSet to record state, or enable checks only in the development environment.
15. Practical Development Advice
Advice 1: Prioritize Reducing Unnecessary Object Creation
For example:
function add(a, b) {
return {
x: a.x + b.x,
y: a.y + b.y
};
}
Can be changed to:
function addTo(out, a, b) {
out.x = a.x + b.x;
out.y = a.y + b.y;
}
This is usually simpler than introducing a full object pool.
Advice 2: Use Object Pools Only in Hot Paths
Only consider object pooling in code like:
- Code executed every frame;
- Large loops;
- Physics and rendering calculations;
- High-frequency network messages;
- Entities like particles, bullets that appear repeatedly.
Advice 3: Set an Upper Limit on the Pool
Do not let the object pool grow unboundedly:
const pool = new ObjectPool(
() => ({ x: 0, y: 0 }),
item => {
item.x = 0;
item.y = 0;
},
100,
1000
);
If the pool has no upper limit, abnormal traffic can cause the pool to hold a large number of objects for a long time.
Advice 4: Do Not Release Objects That Are Still in Use
The most serious problem with object pools is usually not performance, but data errors. Be clear about:
Whoever acquires is responsible for releasing;
Do not continue using after release;
Asynchronous tasks must not directly hold pooled objects.
Summary
The essence of an object pool is:
Reduce repeated memory allocation and garbage collection pressure by reusing objects
It is very suitable for:
- Particles;
- Bullets;
- Enemies;
- Temporary vectors;
- High-frequency messages;
- Animation and physics calculation objects.
But an object pool is not a universal optimization. Modern JavaScript engines have already optimized considerably for short-lived objects. In ordinary business code, directly creating objects is often simpler and more reliable.
A recommended optimization order is:
- First confirm whether a performance problem exists;
- Use Performance and Memory tools to locate the bottleneck;
- Prioritize reducing unnecessary object creation;
- Try reusing output objects or using fixed arrays;
- Introduce an object pool when high-frequency allocation and GC pressure actually exist;
- Clearly define the object lifecycle and improve reset, release, and capacity limits;
- Compare real data before and after optimization, rather than looking only at theory.
Only in scenarios where "object creation is frequent, lifecycle is short, structure is fixed, and lifecycle is controllable" does an object pool truly provide high application value.
Top 1 of 2 from juejin.cn, machine-translated. The original thread is authoritative.
Clearing references upon return avoids implicit retention.
There's no need to clear references at all, because this object pool is already planned to live in memory.