Memory Management in JavaScript Games — destroy(), hide(), and Object Pooling
How to Prevent Memory Leaks and Optimize Performance in Limn Engine
📖 Introduction
Memory management is one of the most overlooked aspects of game development. It's easy to focus on features, graphics, and gameplay — but if your game leaks memory, it will eventually slow down, stutter, and crash.
In JavaScript, memory management is "automatic" thanks to garbage collection. But that doesn't mean you can ignore it. In fact, automatic garbage collection can be a trap — if you don't manage your objects properly, you'll create work for the garbage collector, causing frame drops and stuttering.
Limn Engine provides three tools for memory management:
| Tool | Purpose | Use Case |
|---|---|---|
destroy() |
Permanent removal | Bullets, defeated enemies, collected items |
hide() / show()
|
Temporary invisibility | Respawnable enemies, UI panels |
| Object Pooling | Reuse objects | Particles, bullets, frequent spawns |
In this guide, we'll cover all three — what they are, when to use them, and why they matter.
🎯 The Problem: What Happens When You Don't Manage Memory?
Scenario 1: The Infinite Bullet
// ❌ WRONG: Bullets never get removed
function shoot() {
const bullet = new Component(5, 10, "yellow", player.x, player.y, "rect");
bullet.speedY = -500;
display.add(bullet);
bullets.push(bullet);
}
function update(dt) {
for (let i = 0; i < bullets.length; i++) {
bullets[i].y += bullets[i].speedY * dt;
// Bullets keep going forever — even off-screen!
}
}
The Problem: Every bullet you shoot stays in memory forever. After 10,000 bullets, your game will slow to a crawl.
The Fix: Remove bullets when they leave the screen.
// ✅ CORRECT: Remove off-screen bullets
function update(dt) {
for (let i = bullets.length - 1; i >= 0; i--) {
bullets[i].y += bullets[i].speedY * dt;
if (bullets[i].y < -50) {
bullets[i].destroy(); // Remove from memory
bullets.splice(i, 1); // Remove from array
}
}
}
Scenario 2: The Enemy That Won't Die
// ❌ WRONG: Enemies just hide — never destroy
function killEnemy(enemy) {
enemy.hide(); // Disappears, but still in memory!
}
function update() {
// 100 enemies hidden, still being processed every frame
for (let i = 0; i < enemies.length; i++) {
if (enemies[i].health <= 0) {
enemies[i].hide(); // Still in comm[] !
}
}
}
The Problem: Hidden enemies are still in comm[] and still being processed every frame. They just aren't drawn.
The Fix: Use destroy() for permanent removal, hide() only for temporary invisibility.
// ✅ CORRECT: Destroy dead enemies
function killEnemy(enemy) {
enemy.destroy(); // Remove from comm[] completely
}
// Use hide() only for temporary invisibility
function respawnEnemy(enemy) {
enemy.hide(); // Temporary
setTimeout(() => {
enemy.show(); // Reappear
}, 3000);
}
🔧 Tool 1: destroy() — Permanent Removal
What It Does: Completely removes a component from the engine's rendering pipeline.
How It Works:
// Inside Component.destroy()
destroy() {
// Find and remove from comm[] (main display)
let index = comm.findIndex(c => c.x === this);
if (index > -1) comm.splice(index, 1);
// Find and remove from commp[] (fake canvas)
index = commp.findIndex(c => c.x === this);
if (index > -1) commp.splice(index, 1);
// Nullify the update method to prevent accidental calls
this.update = null;
}
When to Use:
- Bullets that leave the screen
- Defeated enemies
- Collected items
- Any object that will never be needed again
Example:
// Remove off-screen bullets
if (bullet.y < -50 || bullet.y > 650 || bullet.x < -50 || bullet.x > 850) {
bullet.destroy();
}
// Remove defeated enemies
if (enemy.health <= 0) {
enemy.destroy();
}
// Remove collected coins
if (player.crashWith(coin)) {
coin.destroy();
}
Why It Matters:
- Prevents
comm[]from growing indefinitely - Reduces the number of objects processed each frame
- Prevents memory leaks
🔧 Tool 2: hide() / show() — Temporary Invisibility
What They Do: hide() stops a component from being drawn. show() makes it visible again.
How They Work:
// Inside Component.hide()
hide() {
this.update = null; // Stop drawing
}
// Inside Component.show()
show() {
this.update = this.bUpdate; // Resume drawing
}
When to Use hide():
- Enemies that respawn
- UI panels that toggle
- Objects that temporarily disappear
- Anything that will be needed again
Example:
// Enemy respawn system
function respawnEnemy(enemy) {
// 1. Hide the enemy
enemy.hide();
// 2. Wait 3 seconds
setTimeout(() => {
// 3. Move to a new position
enemy.x = Math.random() * 700 + 50;
enemy.y = Math.random() * 500 + 50;
// 4. Show the enemy
enemy.show();
}, 3000);
}
// UI panel toggle
function toggleMenu() {
if (menu.visible) {
menu.hide();
menu.visible = false;
} else {
menu.show();
menu.visible = true;
}
}
Why Not Use destroy()?
-
destroy()removes the object fromcomm[] - If you destroy an enemy, you have to recreate it from scratch
-
hide()keeps the object in memory — faster to show again
🔧 Tool 3: Object Pooling — Reuse Objects
What It Is: A design pattern where you pre-create a pool of objects and reuse them instead of creating and destroying them constantly.
Why It Matters:
| Approach | Performance | Memory |
|---|---|---|
| Create/Destroy | ❌ Slow (GC spikes) | ❌ Fragmented |
| Object Pool | ✅ Fast (no GC) | ✅ Efficient |
How It Works:
- Create a pool of objects at startup
- When you need an object, take one from the pool
- When you're done, return it to the pool
- Never create or destroy objects during gameplay
Example: Bullet Pool
class BulletPool {
constructor(size = 50) {
this.pool = [];
this.active = [];
// Pre-create bullets
for (let i = 0; i < size; i++) {
const bullet = new Component(5, 10, "yellow", 0, 0, "rect");
bullet.hide(); // Start invisible
this.pool.push(bullet);
}
}
// Get a bullet from the pool
get(x, y, speedX, speedY) {
let bullet = this.pool.pop();
// If pool is empty, create a new one (expands the pool)
if (!bullet) {
bullet = new Component(5, 10, "yellow", 0, 0, "rect");
}
// Configure the bullet
bullet.x = x;
bullet.y = y;
bullet.speedX = speedX;
bullet.speedY = speedY;
bullet.show();
display.add(bullet);
this.active.push(bullet);
return bullet;
}
// Return a bullet to the pool
return(bullet) {
bullet.hide();
bullet.x = -1000; // Move off-screen
bullet.y = -1000;
this.pool.push(bullet);
// Remove from active array
const index = this.active.indexOf(bullet);
if (index > -1) this.active.splice(index, 1);
}
// Update all active bullets
update(dt) {
for (let i = this.active.length - 1; i >= 0; i--) {
const bullet = this.active[i];
bullet.y += bullet.speedY * dt;
bullet.x += bullet.speedX * dt;
// Return to pool when off-screen
if (bullet.y < -50 || bullet.y > 650 ||
bullet.x < -50 || bullet.x > 850) {
this.return(bullet);
}
}
}
}
// Usage
const bulletPool = new BulletPool(30);
function shoot() {
bulletPool.get(player.x + 20, player.y, 0, -500);
}
function update(dt) {
bulletPool.update(dt);
}
📊 Comparison: Which Approach to Use?
| Scenario | Best Approach | Why |
|---|---|---|
| Bullets | Object Pool | High frequency, needs to be fast |
| Enemies |
destroy() + recreate |
Lower frequency, destroy() is fine |
| Respawnable enemies |
hide() / show()
|
Reuse the same object |
| Particles | Object Pool | High frequency, custom system |
| UI panels |
hide() / show()
|
Toggle visibility |
| Collected items | destroy() |
Never needed again |
| Tiles | destroy() |
Permanent changes |
💡 Pro Tips for Memory Management
1. Always Iterate Backwards When Splicing
// ✅ CORRECT: Reverse iteration
for (let i = enemies.length - 1; i >= 0; i--) {
if (enemies[i].health <= 0) {
enemies[i].destroy();
enemies.splice(i, 1);
}
}
// ❌ WRONG: Forward iteration skips elements
for (let i = 0; i < enemies.length; i++) {
if (enemies[i].health <= 0) {
enemies.splice(i, 1); // Skips the next enemy!
}
}
2. Use destroy() After Removing from Your Arrays
// 1. Remove from your array
enemies.splice(i, 1);
// 2. Then destroy
enemy.destroy();
3. Pre-Allocate Pools for High-Frequency Objects
// Pre-allocate bullets, particles, and other high-frequency objects
const bulletPool = new BulletPool(100); // Pre-create 100 bullets
const particlePool = new ParticlePool(500); // Pre-create 500 particles
4. Profile Your Game
Use the browser's built-in tools:
- Chrome DevTools → Performance → Record
- Look for garbage collection spikes
- If you see regular GC spikes, you need better memory management
🎯 The One-Line Summary
"
destroy()for permanent removal,hide()for temporary invisibility, and object pooling for high-frequency objects — use the right tool for the right job."
🔗 Resources
| Resource | Link |
|---|---|
| 10x Developer Guide | limn-engine-doc.vercel.app/10x.html |
| API Reference | limn-engine-doc.vercel.app/reference.html |
Draw your game into existence — and clean up after yourself. 🚀
Top comments (0)