How Object Pooling Fixes Game Performance
Chapters6
This chapter explains why object pooling is essential for performance in growing games, showing how reusing objects like enemies, particles, and projectiles avoids costly memory allocations and garbage collection while maintaining a smooth game loop.
Object pooling in Franks Laboratory dramatically boosts game performance by reusing objects (enemies, particles, projectiles) instead of repeatedly creating and destroying them.
Summary
Franks Laboratory dives into object pooling as a core performance technique for game development. Instead of constantly spawning and destroying hundreds of enemies, particles, and projectiles, the video demonstrates a generic object pooler that preloads a fixed number of objects and recycles them. The pooler maintains two arrays—pool for inactive objects and active for those in use—and uses a factory function to create new objects only when the pool runs dry. John Frank (Franks Laboratory) explains the importance of avoiding frequent memory allocations and garbage collection, which can cause frame drops. He walks through implementing a reusable pooler with a rest parameter to forward per-object update arguments, and shows how to integrate it with an enemy manager that spawns from the pool, resets objects, and releases them back. Additional practical tips include moving hard-coded values (like a 200-pixel despawn margin) into constants.js for clarity. The example code demonstrates spawning multiple test enemies, validating that pooling works in practice and scales to larger numbers. The video also foreshadows expanding the system to particles, projectiles, power-ups, and other spawns without touching pooler logic. Overall, Franks Laboratory emphasizes modularity, reusability, and performance predictability when building large, object-heavy games.
Key Takeaways
- Pre-create a fixed number of objects (e.g., 20 enemies) and deactivate them to form a ready-to-use pool.
- Two arrays in the pooler—pool for inactive objects and active for currently used ones—allow efficient activation/deactivation cycles.
- The get method activates an object from the pool or creates a new one if the pool is empty, ensuring seamless spawning.
- The updateAll method uses a reverse loop to safely handle objects that may deactivate themselves during updates.
- Objects expose reset and update methods; reset prepares objects for reuse, while update runs only on active objects.
- A generic object pooler accepts a factory function and a pool size, keeping the pooling logic decoupled from object specifics.
- Rest parameters forward varying per-object update arguments (e.g., player for enemies, targets for projectiles) without changing pooler code.
Who Is This For?
Essential viewing for JavaScript game developers looking to optimize performance for games with many on-screen entities, such as enemies, bullets, and particles. It’s especially helpful for those integrating a reusable object pooler into their codebase to keep memory usage predictable.
Notable Quotes
"Every time you create a new enemy, the system also allocates memory for that object."
—Franks Laboratory explains why object creation costs memory and why pooling helps.
"The get method will retrieve an object from the pool when called and return that object."
—Describes how pooling supplies ready-to-use objects.
"This pooler is completely generic and reusable for any type of object without changing a single line of code."
—Highlights the decoupled, flexible design of the object pooler.
"If we loop forward here while removing objects from the array, the indexes would shift and we could skip objects. We loop in reverse for safety."
—Explains why the update loop iterates from the end to the start.
"The despawn margin is a constant to avoid magic numbers and make tuning easy."
—Shows good practice by moving hard-coded values into constants.js.
Questions This Video Answers
- How does object pooling reduce garbage collection pauses in games?
- What is a rest parameter and how is it used in a generic object pooler?
- Can I reuse the same pooler for enemies, bullets, and particles without changing code?
- What are the best practices for setting pool sizes in a game?
- How do you reset pooled objects before returning them to the pool?
Object PoolingJavaScript Game DevelopmentEnemy PoolParticle PoolObject PoolerGarbage CollectionGame Performance OptimizationRest ParametersFactory FunctionsConstants Management
Full Transcript
At first, spawning enemies is simple. Create an object, destroy an object. But, as your game grows, more enemies, more particles, more projectiles, performance becomes really important. Every time you create an object, JavaScript allocates memory. And every time you destroy it, garbage collection has to clean it up. That process is automatic, but it's not free. If we are constantly creating and destroying hundreds of objects every second, those memory allocations and cleanup cycles can interrupt the game loop and cause frame drops. That's where object pooling comes in, one of the most important optimization techniques in game development.
Instead of constantly creating and destroying objects, we reuse them. Let me show you how to implement a simple object pooler that we can use for enemies, particles, projectiles, and anything else we might need in our game. This class is part of the Vanilla JavaScript game development series. The full playlist is linked below the like button. Inside the JS folder, I create a subfolder called utils, utilities. This folder will store small, reusable helper classes and functions that don't belong to a specific game system or entity. These utilities keep the code organized and modular and can be used across multiple parts of the game.
One such tool is the object pooling system. Before we start spawning and destroying enemies, let's understand what object pooling is and why it's incredibly useful, not just for games like Vampire Survivors that have swarms of enemies, particles, and projectiles, but for any game that needs to repeatedly create and remove objects efficiently. Without object pooling, I would have just a simple array of enemies. Whenever I need a new enemy, I would create it using the enemy class and I would push it into that array. When the enemy is destroyed or despawns, I would remove it from the array and JavaScript would automatically reclaim the memory through a process called garbage collection.
Every time I create a new enemy like that, the system also allocates memory for that object. Now, imagine we have 1,000 bullets, 1,000 enemies, and 1,000 particles that are constantly being created and destroyed this way. Frequent memory allocation and garbage collection would become expensive, potentially causing frame drops or stuttering, especially on devices with limited performance. Object pooling solves this problem by reusing objects instead of constantly creating and destroying them, keeping memory usage stable and the game running smoothly. The way object pooling works is that, instead of constantly creating and destroying objects, we pre-create a set number of them, for example, 20 enemies, and we immediately deactivate them.
This gives us a pool of inactive objects ready to be used. When we need a new object, we activate one from the pool. When that object is destroyed or despawns, we deactivate it, reset its properties, and return it to the pool so it can be reused later whenever it's needed. This approach keeps memory allocation and garbage collection to a minimum, improving performance and allowing the game to handle large numbers of objects smoothly. The game you see here is one of the possible directions we can take the code base we're building today. You can see I've added a weapon system with multiple weapon types that auto attack, Vampire Survivors style.
Each level, we get a choice of three random upgrades, and each weapon can level up, allowing us to craft different builds. Right now, we have a basic projectile weapon, a boomerang that damages enemies on the way out and back, an orbiting weapon that protects the area around the player, chain lightning that jumps from enemy to enemy, which we can upgrade to increase the number of jumps, and a bouncing weapon that interacts with enemies and obstacles. Do you have any other weapon ideas? Leave a comment down below. We might end up building this or take the code base in a different direction.
I'm shaping this based on your feedback. So, if you want more episodes, let me know by clicking the like button. We will now create a generic object pooler that can be reused for any type of object, projectiles, particles, power-ups, or anything else your game needs to spawn in large quantities. This system will handle activation, deactivation, and reuse of objects, making it easy to manage large numbers of objects efficiently without repeatedly allocating and destroying them. I export a class called object pooler and give it a constructor. The get method will retrieve an object from the pool when called.
The update all method will loop through all the active objects and call their update method. The release method will deactivate a single object, reset its properties, and return it to the pool for future reuse. The release all method will deactivate and reset all active objects, returning the entire active list to the pool. This setup makes it easy to efficiently manage any objects that need to be created, updated, and reused repeatedly in the game. The constructor takes a factory function to create [clears throat] objects, and I store it as a class property. This factory function can be any function that creates and returns a new object.
The key is that the object pooler doesn't need to know how the factory function works. It only knows that when it calls the function, it will receive a new object to add to the pool. Doing it this way makes the pooler completely generic and reusable for any type of object in the game, enemies, particles, projectiles, and things like that. This factory function will be incredibly simple, probably just one line, so don't be scared if you've never seen this before. Let's just take it step by step. We will write that factory function in a minute when we create the object pools.
But first, let's finish writing this file. As the second parameter, the constructor will take a pool size to pre-populate the pool, how many inactive objects we want to have ready to be used. The key idea here is that the object pooler maintains two arrays, pool, which holds all the inactive objects like enemies or particles waiting to be used, and active, which will hold only the objects that are currently active, that are being updated and drawn in the game. The job of the object pooler is to move objects between these two arrays as the objects get activated and deactivated.
We automatically pre-populate the pool inside the constructor. All of this code runs the moment we create an instance of this class using the new keyword. Inside, we run a for loop. If the pool size is 20, this loop will run 20 times. Each time it runs, it takes this.pool array from line four and pushes one new pooled object into it. Array push method adds whatever we pass to it to the end of an array. I'm passing it this.factory function from line three because, no matter what logic is inside this factory function, we know this function will always return one new object, for example, one new enemy, when called.
So, I take that new object and I push it into this.pool array. And I repeat this 20 times to fill the pool. The get method, whenever it's called, it will activate one object from the pool, push it into the active array, and give us that object we just activated. I create a temporary helper variable called obj. First, I check if there are any objects left in the pool, if there are any inactive objects available. If there are, I call the built-in array pop method, which removes the last element from the array and returns it. If there are no available objects in the pool when get is called, we create a new one by calling this factory function.
I can also add a dev console log so we know the pool wasn't large enough and we had to create an extra object. Then, we take that object, push it into this.active array, and return it. For example, when we want to spawn an enemy, we call get to activate one for us. The method checks if any objects are available in the pool. If so, it gives us one. If none are available, it creates a new one, pushes it into this.active, and returns that object. This way, the pool always gives us a ready-to-use object while keeping track of active ones.
Doing it this way keeps the object pooler completely generic and reusable. The factory function can have different logic depending on what we're creating, an enemy, a projectile, or a particle, but it doesn't matter. We will write this factory function soon, and as long as we make sure each factory function always returns one new object, for example, one new enemy or one new particle, the object pooler can manage it without any changes. The update all method will simply call update on all active objects that need updating. It expects delta time as the first parameter and dot dot dot args arguments as the second.
This second part will need a little explanation. Keep in mind, we are trying to keep this object pooler generic. We want the code in here to work for any type of object pool in our game. For example, if we use it to create a pool of enemies, we want to call each enemy's update method for every frame. All update methods in our game expect delta time as the first parameter, but the second parameter can change depending on the object type. Enemies will expect the player object, projectiles might expect a list of targets, and particles might not expect anything at all.
By using args here, we allow the pooler to pass along any extra data each object needs, while keeping the system flexible and reusable. Here, args basically means any number of additional arguments. In a function parameter like this, this is called a rest parameter. It collects all remaining arguments passed into the function into a single array, so we can forward them into each object's update method without knowing in advance how many there will be or what they are. For example, for enemies, we pass the player. For projectiles, these arguments might represent a list of targets, and for particles, we might not be passing anything at all here.
The rest parameter handles it automatically. Inside update all, we run a for loop that cycles over all objects currently in the active array. We loop in reverse from the end of the array to the beginning because during the update, some objects might deactivate themselves. For example, an enemy might get destroyed or projectile might hit something. If we loop forward here while removing objects from the array, the indexes of the remaining elements would shift, and we could accidentally skip some objects or run into other errors. By looping backward, removing or releasing objects doesn't affect the item we haven't processed yet, so every active object gets updated safely.
We start at the end of the array, which is this.active.length minus one because the arrays are zero-indexed. They start from zero. As long as the index is greater than or equal to zero, we continue cycling backward, reducing the index by one each time, i minus minus. This way, we safely process all active objects even if some are removed from the array during the loop. As the loop runs, we store each object in a temporary variable obj, which points to each item in the array one by one as the index changes. We do this so we can call update on every active object.
It's important that all pulled objects in our project have an update method that expects delta time as the first parameter. The other parameters that follow after that can vary. The rest parameter handles this variability, allowing the pooler to forward any extra data each object might need. If the object is no longer active, maybe it was destroyed, despawned, or its lifetime ended, we call release on it. This method will remove the object from the active array and push it back into the pool array. Releasing an object basically means deactivating it. So, let's write that logic now.
The release method will expect the object we want to deactivate as a parameter. First, it tries to find that object inside the active array. The index of method will return the position of the object in the array or minus one if it's not found. We save it as a temporary index variable. If the index is greater than minus one, which means we successfully found the object in the active array, we use splice to remove one element at that index. This effectively removes the object from the active list. Next, we call the reset method on the object.
We need to make sure that all pulled objects in our game, enemies, particles, projectiles, and so on have a reset method that restores their default state. Finally, we push the object into the pool array, which holds all inactive objects. So, when release is called, the object is removed from active, reset to its default values, and stored in the inactive pool, ready to be reused later. The release all method will take all the objects from the active pool defined on line five and put them back into the inactive pool defined on line four. Inside, we cycle over all the objects in the active array, call reset on each one, and push each object into this.pool.
After the loop, we set this.active to an empty array to make sure there are no objects left and the active array is always empty. This is our generic object pooler. We wrote this so that it can be reused for any type of object without ever having to touch this code again. We can use it for enemies, projectiles, particles, power-ups, and so on. This file is completely separate, modular, flexible, and ready to handle large numbers of objects efficiently. So, how do we actually use the object pool to pool and reuse enemies? First of all, each pulled object will need to have a flag that marks it active or inactive.
Inside the enemy class constructor, I define a new property called this.active and initially set it to false. When this enemy is first created, it will be deactivated. When we spawn an enemy, we set this.active to true like this. We know that all pulled objects will need a reset method. That method is called inside the object pooler here on line 37. And also on line 43. This method will run when we return this reusable enemy object back to the pool of inactive ready-to-use objects. Inside, I set this.active to false and reset enemy health to the value from enemy data like this.
This update method on enemy will run only when the enemy is active. So, if this.active is false, exclamation mark here means not, so not active, we return early, which means the rest of the update method will not run. If the enemy is just sitting in the pool of inactive objects, there is no need for all this code to execute. We will have enemies with all different kinds of movement patterns, so if the enemy wanders too far off screen, we will despawn it and return it to the inactive pool. If the enemy's horizontal X position is less than minus 200, the enemy moved more than 200 pixels to the left of screen, we know we can deactivate it.
Or operator, if the enemy's X position is more than game width plus 200, the enemy moved more than 200 pixels beyond the right edge of the game area, we can also deactivate it. I do the same vertically. If more than 200 pixels beyond the top edge or more than 200 pixels beyond the bottom edge, or operator means if at least one of these four conditions is true, if the enemy moved 200 pixels outside the visible game area in any of the four directions, we can deactivate it. This 200 here is a magic number with no context, so it's better to put it inside constants.js.
I create a new constant called enemy despawn margin, and I set it to 200. Here, I will have to import game width, game height, and enemy despawn margin from constants.js because we are using all of these here. I replace the hardcoded 200 with the constant like this. If we have huge enemy bosses in our game that are more than 200 pixels wide, we would have to increase this value to make sure enemies are fully off screen before we remove them. If at least one of these checks is true, we deactivate the enemy and return early to make sure the rest of the code doesn't run on an inactive enemy object.
Inside enemy manager.js, we will manage these object pools. I will import object pooler from the utils folder like this. I define a helper constant called enemy pool size, and I set it to 10 for now. Then, I create a pool of reusable enemy This.pool will be equal to new object pooler. We know that the object pooler constructor expects two parameters, a factory function and a pool size. So here, I'll pass it a factory function as the first argument and enemy pool size of 10 as the second argument. This factory function needs to contain the logic that creates one new enemy object.
That's very simple, so I can define the function directly in line. It might look a bit unusual at first, but this entire function is the first argument we're passing into the object pooler constructor, and the pool size is the second argument. I'll give myself a bit more space so it's easier to read. To create an enemy, we simply use new enemy and pass it the relevant enemy data just like we did before. We use the return keyword to make sure this function actually returns that new enemy object whenever it's called. And since the pooler will now handle object creation for us, I can delete this old line where we were creating enemies manually.
So here, we're passing the factory function as the first argument, a simple function that whenever called, creates one new enemy and returns it. The second parameter is the pool size, how many enemies we want to pre-create for this enemy pool. The object pooler takes that factory function reference and stores it inside the class. It then uses that function when pre-populating the pool inside the constructor and also later if it ever needs to create additional enemy objects when the pool runs out. Remember, the code inside the constructor runs automatically as soon as we create an instance of the class using the new keyword.
So, all of this happens immediately. When this line executes. At this point, we already have a pool of 10 enemy objects stored inside this.pool. By passing the factory function from here instead of hardcoding enemy creation logic inside the object pooler, we keep the pooler completely generic. That means we can reuse it for particles, projectiles, or anything else in the game without changing a single line of code inside the object pooler file. So, enemy manager will have a spawn method that, when called, spawns one enemy. But, this time it's not hardcoded like we had it before.
Instead of creating a new enemy manually, we take one from the pool we stored on line 10. We do that by calling this.pool.get. We already know how this method works. It checks if there are any available objects in the inactive pool. If there are, it takes one from there. If the pool is empty, it will create an additional enemy using the factory function. So, we always get a valid enemy object when we need one. This way, spawning is no longer responsible for creating enemies. It's simply requesting one from the pool, which keeps our system clean and efficient.
Once we have that enemy, we call its own spawn method and pass it X and Y coordinates along like we did before. This will call the spawn function we defined on the enemy class here, which will position the enemy and mark it as active. Then, this method will return the enemy we got from the pool and spawned it in the game. Get active enemies will be simple. We know that all active enemies are always inside the active array. So, we simply return this.pool.active, which points to this array on line five inside the object pooler. Keep in mind, this.pool holds all inactive enemies.
This.active holds all active enemies. And object pooler manages this by moving the pooled enemy objects between these two arrays depending on their active or inactive status. Get active enemies is really just exposing the active list, so the other parts of the game can loop over currently active enemies when needed. The update method will take delta time and player as parameters. And it will take this.pool from line 10 and call update all. We know it expects delta time and optional additional arguments, args. So, we pass these values along like this. Notice how we pass delta time and player here.
But, inside the object pooler, it's defined as a delta time and args. That's intentional. The second parameter doesn't have to be the player. When we're pooling particles or projectiles, this could be something else entirely. The rest parameter syntax, dot dot dot args, handles forwarding whatever additional data is needed. This keeps our enemy manager clean while the object pooler stays completely flexible and reusable. I also give enemy manager a reset method, which will take the object pool and call release all. We know that method will simply reset all objects inside the active array and move them into the inactive pool, leaving the active array completely empty.
I go to game.js and down here inside the start game method, I call enemy manager.reset. Which will in turn call this.pool.releaseAll. This makes sure all pooled enemy objects are deactivated and reset to their default state when the game starts. Just for testing, I will duplicate this line several times and pass each one a different set of X and Y coordinates like this to spawn multiple test enemies. This will not be the final implementation. We will create a proper enemy spawner in the next step. But, let's run our code to make sure everything is working and we get no errors, so we can mark this as a nice complete checkpoint.
When I play, I get 1 2 3 4 5 and 6 enemies. Perfect. Some of the coordinates I set are way outside the visible area, but since all the enemies chase the player, they eventually come into view. So, instead of constantly creating and destroying enemies over and over, we now have a pooling system. We can use and reuse the same enemy objects efficiently without unnecessary memory allocation and garbage collection. We also wrote the object pooler in a completely generic and reusable way, which means we can use the exact same file for projectiles, particles, [music] power-ups, or anything else we might need in our game without changing a single line of code inside that file.
This is a major optimization technique. It allows us to scale up our game dramatically, potentially handling hundreds of enemies on screen and complex particle effects while keeping performance smooth [music] and predictable.
More from Franks laboratory
Get daily recaps from
Franks laboratory
AI-powered summaries delivered to your inbox. Save hours every week while staying fully informed.





