My Chrome tab literally froze for a solid minute before the out-of-memory error popped up. I was trying to push the new Phaser 4.0.2 renderer past its breaking point on my M2 MacBook running Sonoma 14.2. I wanted a million animated sprites on screen at once.
It crashed. Hard.
Well, that’s not entirely accurate — I spent most of last weekend tearing apart my rendering logic trying to figure out why. The promise of the new WebGPU pipeline in Phaser 4 was massive parallel rendering, but my naive approach was choking the main thread before the data even reached the GPU.
Here’s what actually works when you need to render an absurd amount of objects without melting your users’ processors.
The Problem with Standard Sprites
And if you’re used to Phaser 3, your muscle memory tells you to just loop through a group and create Phaser.GameObjects.Sprite instances. Don’t do this for large numbers. Every standard sprite carries a massive payload of properties — physics bodies, input zones, rotation matrices, tint values.
I benchmarked the standard approach just to see where the ceiling was. On my machine, standard sprites maxed out at about 85,000 before the frame rate dipped below 30fps. To hit a million, you have to bypass the standard display list entirely and talk directly to the new GPU instancing API.
First, you need to make sure you’re actually forcing the WebGPU renderer. The fallback to WebGL2 is automatic, but if you’re building specifically for massive scale, you want to fail fast if WebGPU isn’t available.
import Phaser from 'phaser';
// Grabbing our container the old-fashioned DOM way
const gameContainer = document.getElementById('game-app');
const config = {
type: Phaser.WEBGPU, // Force WebGPU, don't use AUTO
width: 1024,
height: 768,
parent: gameContainer,
backgroundColor: '#0a0a0a',
scene: {
preload: preload,
create: create,
update: update
},
// Required for the massive buffer sizes we're about to use
batchSize: 1000000
};
const game = new Phaser.Game(config);
Fetching the Instance Data
You can’t generate a million random positions on the main thread during the create function without locking up the browser. I learned this the hard way. The screen stays black for five seconds, and users assume your game is broken.
Instead, pre-compute your initial state data and load it asynchronously. I wrote a small Go script to generate a binary payload of X/Y coordinates and velocities, but for this example, let’s look at how you pull that data in using the Fetch API and parse it in a Web Worker, or at least asynchronously.
async function fetchSwarmData() {
try {
// Fetching a pre-computed binary file of 1M positions
const response = await fetch('/assets/data/swarm_initial.bin');
if (!response.ok) {
throw new Error(HTTP error! status: ${response.status});
}
const buffer = await response.arrayBuffer();
// Float32Array is exactly what the GPU buffer wants
return new Float32Array(buffer);
} catch (e) {
console.error("Failed to load swarm data. The server probably timed out.", e);
return null;
}
}
The Undocumented Memory Gotcha
Here is where I wasted three hours.
And when you create a custom GPU pipeline for instanced rendering, Phaser allocates a massive chunk of VRAM. If you restart the scene — say, when the player dies — that memory doesn’t automatically clear. The garbage collector doesn’t know what to do with it because it lives on the GPU.
The docs barely mention this. If you don’t manually call .destroy() on your custom pipeline and clear the texture atlases before the scene shuts down, your VRAM fills up. By the third scene restart, my memory usage spiked to 4GB and the tab just died silently. No error. Just a white screen.
You have to hook into the scene’s shutdown event.
function create() {
this.events.on('shutdown', () => {
if (this.swarmPipeline) {
// DO NOT FORGET THIS
this.swarmPipeline.destroy();
this.swarmBuffer = null;
}
});
}
Writing the Instanced Renderer
To actually draw the million sprites, we use a single texture and tell the GPU to draw it a million times, passing a buffer of different coordinates for each instance. This is instancing.
The code looks weird if you’re coming from traditional game dev. You aren’t manipulating objects. You’re manipulating a massive typed array.
let instanceData;
let gpuSpriteBatch;
async function create() {
// Load our single tiny 8x8 pixel texture
const tex = this.textures.get('spark').getSourceImage();
// Grab the data we fetched earlier
instanceData = await fetchSwarmData();
if (!instanceData) return;
// The new Phaser 4 InstancedSprite API
gpuSpriteBatch = this.add.instancedSprite(0, 0, 'spark');
// We pass the raw Float32Array directly to the GPU
// Format: [x, y, rotation, scale, alpha] per sprite
gpuSpriteBatch.setInstanceData(instanceData, 1000000);
console.log("1M Sprites pushed to GPU");
}
function update(time, delta) {
if (!gpuSpriteBatch) return;
// We update the buffer in a web worker in a real game,
// but for simple movement, a custom shader handles the math.
// We just tell the pipeline what time it is.
gpuSpriteBatch.pipeline.set1f('uTime', time / 1000.0);
}
Notice that the update loop has almost nothing in it. I offloaded all the movement logic to a custom vertex shader. If you try to loop over a million items in JavaScript 60 times a second to update their X and Y coordinates, you will fail. V8 is fast, but it’s not that fast.
By passing the uTime uniform to the shader, the GPU calculates the new positions based on math (like sine waves or noise functions) rather than relying on the CPU to tell it where every single pixel should go.
The Results
Once I fixed the memory leak and moved the position logic to the shader, it worked. I hit 1.2 million animated sprites at a solid 58fps.
It’s honestly wild seeing JavaScript handle this kind of load. Just a few years ago, trying to render 10,000 particles in Canvas was a struggle. Now we’re throwing around millions of instances like it’s nothing.
Probably, by Q1 2027, I doubt anyone will be building 2D web games with standard DOM or Canvas renderers anymore. WebGPU is just too fast to ignore, and the older WebGL fallbacks will start feeling like Internet Explorer hacks.
Just remember to clean up your VRAM. Seriously. It will save you a very frustrating weekend.
