Speed vs. Features: When to Swap a PixiJS Container for a ParticleContainer
Rendering thousands of sprites in PixiJS can choke performance. Learn when to swap a plain Container for a ParticleContainer, the trade‑offs you’ll face, and a concrete example that keeps FPS high.
15 Nov 2025, 15:25 UTC

Why the Switch Matters
In a 2‑D web game you’ll often need to show thousands of tiny, moving elements – sparks, bullets, falling leaves. If you add each of those as a Sprite to a plain Container, the renderer has to process a separate transform, depth, and draw call for every child. On modern GPUs that isn’t a problem, but the JavaScript side still pays a per‑child cost: the engine must walk the child list, calculate bounds, and push uniforms for each sprite. The result is a noticeable drop in frame‑rate when the count climbs past a few thousand.
The practical takeaway: use ParticleContainer for high‑count, texture‑shared scenes. It trades a handful of visual features for a huge performance boost.
What ParticleContainer Gives You
ParticleContainer is a lightweight, batch‑friendly subclass of Container. Internally it stores all child sprites in a flat array and builds a single WebGL buffer that contains only the data you tell it to keep – position, rotation, scale, tint, and alpha. Because it knows the data layout in advance, the renderer can upload the whole buffer in one call and draw the whole group with a single draw call.
Key points:
- All children must share the same
Texture(or a texture atlas). If you need different images, keep them in separate containers. - The container only updates the properties you enable. By default it tracks
position,rotation,scale,alpha, andtint– you can disable any of them to reduce memory usage. - Because the renderer skips per‑child state changes, the JavaScript overhead drops from O(n) to O(1) per frame.
What You Give Up
Every optimisation has a cost. ParticleContainer deliberately disables or limits several features that a normal Container would support:
- Filters and masks – The container cannot apply a
Filteror aMaskto individual children. Only the whole container can be filtered or masked. - Blend modes per child – All children share the container’s
blendMode. Changing a single sprite’s blend mode is not possible. - Interactivity – Events such as
pointerdownon a child are not routed through the container. If you need clickable particles, you’ll need a separate interactive layer. - Advanced tinting – While you can set a global tint on the container, per‑sprite tint changes are limited to a single
tintvalue per sprite.
These restrictions make ParticleContainer ideal for decorative or purely visual effects, not for UI elements or interactive bullets that must react to user input.
Concrete Example: A Simple Firework
Below is a minimal, self‑contained example that shows how to create 5,000 spark particles with a single texture, update them each frame, and render them efficiently. The code targets PixiJS v8.
import { Application, Container, Sprite, Texture, ParticleContainer } from 'pixi.js';
// 1. Create the Pixi app
const app = new Application({ width: 800, height: 600, backgroundColor: 0x111111 });
document.body.appendChild(app.view);
// 2. Load a simple spark texture (a small white circle)
const sparkTex = Texture.from('spark.png'); // replace with your own asset URL
// 3. Create a ParticleContainer that only tracks position, rotation, and alpha
const particleCount = 5000;
const particles = new ParticleContainer(particleCount, {
scale: false, // we don’t need per‑particle scaling
rotation: true,
position: true,
alpha: true,
tint: false,
});
app.stage.addChild(particles);
// 4. Create JavaScript arrays to hold particle state
const posX = new Float32Array(particleCount);
const posY = new Float32Array(particleCount);
const velX = new Float32Array(particleCount);
const velY = new Float32Array(particleCount);
const life = new Float32Array(particleCount);
const alpha = new Float32Array(particleCount);
// 5. Initialise particles with random positions and velocities
for (let i = 0; i < particleCount; i++) {
const sprite = new Sprite(sparkTex);
sprite.anchor.set(0.5);
particles.addChild(sprite);
posX[i] = Math.random() * app.screen.width;
posY[i] = Math.random() * app.screen.height;
const speed = Math.random() * 2 + 1;
const angle = Math.random() * Math.PI * 2;
velX[i] = Math.cos(angle) * speed;
velY[i] = Math.sin(angle) * speed;
life[i] = Math.random() * 2 + 1; // seconds
alpha[i] = 1;
}
// 6. Update loop
app.ticker.add(delta => {
const dt = delta / 60; // convert to seconds
for (let i = 0; i < particleCount; i++) {
// Simple physics
posX[i] += velX[i] * dt;
posY[i] += velY[i] * dt;
life[i] -= dt;
// Fade out as life ends
alpha[i] = Math.max(0, life[i] / 2);
// Remove off‑screen particles by wrapping around
if (posX[i] < 0) posX[i] += app.screen.width;
if (posX[i] >= app.screen.width) posX[i] -= app.screen.width;
if (posY[i] < 0) posY[i] += app.screen.height;
if (posY[i] >= app.screen.height) posY[i] -= app.screen.height;
}
// Push state into sprites
for (let i = 0; i < particleCount; i++) {
const sprite = particles.getChildAt(i);
sprite.position.set(posX[i], posY[i]);
sprite.alpha = alpha[i];
}
});
Key optimisation tricks used here:
- All particle state lives in typed arrays, so the garbage collector never sees per‑frame allocations.
- We only update
positionandalpha– both are enabled in the container options. - The container’s
scaleandtintare disabled, reducing the size of the GPU buffer.
Measuring the Gain
To confirm the performance benefit, run the example on two setups:
- Replace the
ParticleContainerwith a plainContainerand keep the same number of sprites. - Open Chrome DevTools, go to the Performance tab, and record a 5‑second session on each version.
Look for:
- Average FPS – a
Containerversion will drop below 30 fps on a mid‑range GPU, while theParticleContainerstays above 60 fps. - GPU usage – the
ParticleContainershows a single draw call; the plain version shows thousands. - CPU usage – the plain version has a higher
JS Threadload due to per‑child processing.
Run the test on a low‑end device or throttle your CPU to expose the bottleneck more clearly.
When the Trade‑Offs Matter
If your particles need interactivity (click, hover) or custom filters, you’ll have to keep them in a normal Container or split the scene: use a ParticleContainer for the bulk of the effect and a separate Container for the interactive subset.
Similarly, if you want per‑particle blend modes or dynamic tint changes, a ParticleContainer won’t suffice. In those cases, consider a hybrid approach: keep the bulk of the particles in a ParticleContainer and move the special ones to a separate container.
Actionable Takeaway
When you’re rendering thousands of identical sprites:
- Use
ParticleContainerand keep the feature set you need – position, rotation, alpha, maybe tint. - Disable unused properties to shrink the GPU buffer.
- Store particle state in typed arrays and batch updates to avoid per‑frame allocations.
- Profile on your target devices; the gains are biggest on low‑end GPUs.
- If you need interactivity or filters, isolate those particles in a regular
Containerand keep the rest in aParticleContainer.
With these steps you’ll keep your frame‑rate high while still delivering a visually rich particle effect.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.