skills/pixijs-scene-core-concepts/references/render-groups.md
A render group is a Container whose transform (position, scale, rotation, alpha, tint) is applied on the GPU as a single operation rather than being recalculated per-child on the CPU every frame. Use render groups for large stable sub-trees such as a game world, a HUD, or a parallax strip. Each render group owns its own instruction set and cannot batch with other groups.
const world = new Container({ isRenderGroup: true });
const hud = new Container({ isRenderGroup: true });
app.stage.addChild(world, hud);
for (let i = 0; i < 5000; i++) {
const bunny = new Sprite(bunnyTexture);
bunny.x = Math.random() * 2000;
bunny.y = Math.random() * 2000;
world.addChild(bunny);
}
world.x = 100; // applied once on the GPU, not 5000 times on the CPU
Flag a container with isRenderGroup: true at construction, or call enableRenderGroup() at runtime.
const world = new Container({ isRenderGroup: true });
const hud = new Container();
hud.enableRenderGroup();
Both forms work. Pre-declare via the options object for static setups; call enableRenderGroup() when you want to flip the flag at runtime.
When a render group is in the chain, PixiJS composes three matrices:
localTransform; from the container's own position, scale, rotation, pivot, skew.groupTransform; the child's position relative to the render group it belongs to.worldTransform; the scene-root-space matrix.For children inside a render group, the CPU only maintains localTransform and groupTransform. The GPU applies the group's own worldTransform once, then draws all the children. This is the performance win: the per-child CPU matrix math is cut from "one full walk per frame" to "one walk when children move."
Good fits:
Bad fits:
// Start without explicit render groups
const world = new Container();
// Measure frames, identify bottleneck
// Add if CPU transform updates dominate
world.enableRenderGroup();
Most scenes don't need explicit render groups beyond the auto-created root. The scene root container passed to renderer.render() is automatically a render group. Add explicit groups only when profiling shows CPU transform costs dominating.
const hud = new Container();
hud.cacheAsTexture({ antialias: true });
const world = new Container({ isRenderGroup: true });
cacheAsTexture() rasterizes the sub-tree once to a texture and draws that texture each frame. Best for things that don't change visually.isRenderGroup keeps normal drawing but moves the group transform work to the GPU. Best for sub-trees whose children animate freely but whose structure is stable.Render groups are lighter than cacheAsTexture and preserve dynamic children. Use cacheAsTexture only when the sub-tree is genuinely static.
Wrong:
for (const enemy of enemies) {
enemy.enableRenderGroup();
}
Each render group has its own instruction set and can't batch with other groups. A scene with many small render groups generates many separate draw buckets, which is slower than a single batched draw. Apply render groups at a coarse level (world, HUD), not per-entity.
Wrong:
const world = new Container({ isRenderGroup: true });
const section = new Container({ isRenderGroup: true });
const tile = new Container({ isRenderGroup: true });
world.addChild(section);
section.addChild(tile);
Correct:
const world = new Container({ isRenderGroup: true });
const section = new Container();
const tile = new Container();
world.addChild(section);
section.addChild(tile);
Deep nesting of render groups multiplies the instruction set overhead. Pick a single level (usually one per "subsystem") and let the children be normal containers.
If the sub-tree structure changes every frame (not just child transforms), the render group's instruction set is rebuilt each time, negating the performance benefit. Render groups help stable structures with animated children; not dynamic structures.